乐者为王

Do one thing, and do it well.

Ruby调试工具概览

调试Ruby代码最简单的方式就是使用puts或p方法。当有很多变量需要查看时,到处添加puts或p方法就可能变的不那么实际。幸好,Ruby社区提供了许多强大的调试工具。

Ruby 1.8+时代

调试Ruby代码使用ruby-debug。调试Rails代码则是pry-nav。不过在Ruby 1.9出来后ruby-debug就有问题了,于是就有了ruby-debug19,一个针对Ruby 1.9的ruby-debug移植版本。

Ruby 1.9.2+时代

等到Ruby 1.9.2发布,ruby-debug彻底歇菜,然后debugger就出现了。pry-nav也不好使了,还好有pry-debugger

Ruby 2+时代

新的Ruby调试工具byebug来了。虽然byebug也能调试Rails应用,但它不提供语法高亮,所以使用pry-byebug是个更好的选择。

Ruby 1.8+ Ruby 1.9 Ruby 1.9.2+ Ruby 2+
Ruby ruby-debug ruby-debug19 debugger byebug
Rails pry-nav pry-nav pry-debugger pry-byebug

其它

Pry其实不是纯粹的调试工具,它只是IRB的替代品,所以缺乏必要的调试指令。pry-nav、pry-debugger和pry-byebug做的只是分别把ruby-debug、debugger和byebug中的step、next、continue等指令添加到Pry中。

  • pry-nav = Pry + ruby-debug
  • pry-debugger = Pry + debugger
  • pry-byebug = Pry + byebug

如果要调试view怎么办?可以使用Web Console。在view里面加上<%= console %>,当view出现异常时,就会在异常界面下方,出现一个网页版的IRB,方便调试。Web Console默认只接受localhost的请求,假如需要让别的IP也能访问的话,可以这样做:

1
2
3
class Application < Rails::Application
  config.web_console.whitelisted_ips = '192.168.0.100'
end

如何使用VisualVM检测Java内存泄漏

Java的一个重要优点是通过垃圾收集器(Garbage Collection)自动管理内存的回收,程序员不需要关注它。程序员真的不需要关注内存管理吗?只要你碰到过OutOfMemoryError你就知道它不是真的。

这里我会展示如何使用VisualVM快速定位内存泄漏。先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.List;
import java.util.ArrayList;

public class MemoryLeakDemo {
    public static void main(String[] args) {
        new Thread(new MemoryLeak(), "MemoryLeak").start();
    }
}

class MemoryLeak implements Runnable {
    public static List<Integer> leakList = new ArrayList<Integer>();

    public void run() {
        int count = 0;
        while (true) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
            }
            count++;
            Integer i = new Integer(count);
            leakList.add(i);
        }
    }
}

执行下列命令:

1
java -verbose:gc -XX:+PrintGCDetails -Xmx20m MemoryLeakDemo

等待一段时间后,你会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Exception in thread "MemoryLeak" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:3181)
        at java.util.ArrayList.grow(ArrayList.java:261)
        at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
        at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
        at java.util.ArrayList.add(ArrayList.java:458)
        at MemoryLeak.run(MemoryLeakDemo.java:22)
        at java.lang.Thread.run(Thread.java:745)
Heap
 PSYoungGen      total 3584K, used 298K [0x00000000ff980000, 0x00000000ffe80000, 0x0000000100000000)
  eden space 3072K, 9% used [0x00000000ff980000,0x00000000ff9ca908,0x00000000ffc80000)
  from space 512K, 0% used [0x00000000ffc80000,0x00000000ffc80000,0x00000000ffd00000)
  to   space 512K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000ffe80000)
 ParOldGen       total 13824K, used 12156K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
  object space 13824K, 87% used [0x00000000fec00000,0x00000000ff7df3e8,0x00000000ff980000)
 Metaspace       used 7993K, capacity 8164K, committed 8448K, reserved 1056768K
  class space    used 912K, capacity 954K, committed 1024K, reserved 1048576K

打开VisualVM开始监测MemoryLeakDemo,在Monitor标签页我们可以看到实时的程序内存堆的使用情况:

波峰到波谷处是执行了GC的,明显可以看到执行GC后内存曲线仍旧呈上扬趋势,也就是说,内存占用是只升不降。到底是什么原因导致的呢?

打开Sampler标签页,点击Memory按钮启动一个内存分析会话,VisualVM会定期获取所有执行线程的转储,分析栈跟踪信息,实时显示成堆直方图。通过堆直方图,我们就可以知道哪个对象占用了较多的内存,以便做进一步的优化。

如上图所示,第1行的Integer对象占用内存最大,已经有41万多实例了,并且还在持续增加中。很显然,罪魁祸首就是它了!

对抗完美

英文原文:http://usabilitypost.com/2008/10/08/fighting-perfection/

如果你像我一样,你可能经常会发现自己从来没有完全满意你的工作——总是做出调整和修改,总是找到你不太喜欢的事情然后改进他们。这适用于很多创造性的努力——或许你正在为你的博客加工一篇文章,整理一份报告或者写一封重要的电子邮件。

问题是,即使在作出修改后,仍然有一些事情你可以调整,事情仍然不是相当完美。

这当然是很好的,你给自己设置了一个高标准。如果你不满意你的工作,那么为什么你的访客或客户就要满意呢?

追求完美是件好事

一个建筑师拥有的两个最重要的工具是绘图室的橡皮擦和工地的大铁锤。

Frank Lloyd Wright

Steve Jobs不满意iPhone的第一个版本。他做了一个艰难的决定去放弃最初的设计,因为他不喜欢它;他觉得这不是Apple能做到的最好的。这引起了很多问题给开发团队,因为他们必须在很短的时间范围内整理出一份全新的设计。

新版本获得了成功,要是他没有做出这个艰难的决定,iPhone将不会成为手机行业的大标志,这在一定程度上要归功于它的标志性设计。

但是……

完美是困难的和费时的

完美可以是危险的和误导的。什么时候足够好?什么时候你可以前进,释放你应用的新版本或发布你的新文章?完美是太高的一个目标,因为它简直太难和太费时去实现。

如果你成了完美的奴隶,你会发现你所有的时间被耗尽。你会不停地做修改调整调整修改——事情却没有按时做完。

如何对抗完美?

考虑优先级——什么是真正要紧的事?对于一个非常大的公司,类似iPhone的东西是一个关键的产品;如果你搞砸了,它能造成严重损失。把产品做正确是至关重要的。设定一个非常高的标准在这里将是一个不错的主意。

那么更小的事情像是博客的设计呢?最终,它通常并不重要,除非这个博客是你的主要业务。在这里简单是你的盟友。简单的东西很难被搞砸,因此创建简单的事情然后把工作做完。

你最宝贵的资源是你的时间。为了对抗完美你必须将时间排出优先次序,并专注于那些要紧的事情。如果你正在做的和改进的事情没有那么重要,那么这些事情就不应该去做。

把事情做完

做完。开始把它当作一个咒语。当你去做完它时意味着某些事情已经被完成。决定已经做出,你可以继续前进。做完意味着你正在累积动力。

37signals, Getting Real

执行比想法更重要。把足够好的东西释放出来比做完美但从未完成的东西更好。不要在你做的每件事情上寻求完美——除了那些真正要紧的事情。驯服完美——更快地做完其它的每件事情,把节省下来的时间用在你最重要的项目上。

你能真正地掌握多少编程技术?

英文原文:http://thecodist.com/article/how-many-programming-technologies-can-you-really-master

我总是看见公司或者它们的招聘人员广告他们正在寻找的人:“有从零开始开发iOS和Android应用的丰富经验,必须掌握现代移动和Web技术,包括Java、HTML5、CSS3、JavaScript、JSON和AJAX”。

没有这样的人。你可以掌握一门技术但在其它方面平平;你可以掌握一门技术然后转向另一门技术但是会忘记很多先前的技术;你可以简单地欺骗足够多的人让他们认为你能做到,然后期望你恰巧能够搞定它。

在今天,任何主要领域的编程都是高度复杂的,不断变化的,并且通常是带着很大时间压力完成的。所有这些都不允许你投入大量非编程时间去学习即使是最新的变化,更不用说从零开始掌握一切。你只能通过做真实的项目了解新环境,有多少人能够在同一时间同时编写所有大型的本地Android、本地iOS和响应式Web客户端呢?

在我作为程序员的34年里,我很少工作在超过一个主要领域。我的第一份工作是在一台supermini上,然后Apple上的6502汇编和在一台PC上的Pascal,我的两个创业公司都是用C为Mac开发,我更多地为其他人(包括Apple)工作,用C为Mac开发,一点点C++,接着从Objective-C/WebObjects转换到用Java开发Web客户端和服务端(尽管两者都很少),一些JavaScript,然后在Mac和Windows上进行C++游戏编程,最后是Objective-C和iOS。每次转换都是匆匆忙忙地大量学习,接着是年复一年的掌握所有新的东西。

如果由于某种原因,有人确实能做Android和iOS两者——更不用说Web——以一个真正的大师水平,他们应该能赚比大多数公司愿意支付的更多的钱。公司想要的是雇几个能做所有事情的人,并且以他们能够得到的最低的工资水平。然而我无法理解有人能够同时在这么多事情上成为一个专家,以及他们如何能够用多种技术编写多个应用并且坚持下去。我认识一些极其聪明的人,但我不记得有人棒到确实能在同一时间兼顾不相关的技术并且产出技艺精湛的应用。

也许会有例外。但我仍然认为大多数人做不到。人们当然可以掌握一件事情然后转向掌握另一件事情,但在这个过程中你不可避免地会忘记前者的细节。去年,我在等待裁员且没有什么事情要做的几个月里(我是最后无缘无故裁员中的一个,因为在品牌的最终出售之前我们所有的技术已经被换掉了),我花了一个月的时间在C++上,然后Node.js,最后Swift。今年继续这些语言(因为我的新工作做的是Objective-C)我发现我已经忘了大部分我所学过的。如果你不经常使用某样东西,记忆似乎丢到了脑后。在用PHP重写这个博客引擎的过程中我在我脑中把所有这些语言都搞混了。

如果你是在iOS上从Objective-C转到Swift,那至少还有些重叠。但Android和iOS不仅是不同的语言,一切都不同,从工具到如何布局去支持多个主要的OS发行和24,000多种不同的设备。仅仅是跟上所有这些年在六月WWDC的新变化就要花费大量的精力;雪上加霜的是Apple释放的示例代码在最新beta版的XCode里已经不能编译。就算你不写代码整天只是观看视频和阅读文档与示例代码,你怎么能一本正经的说你是一个专家呢?

给两个不同的移动OS环境添加复杂混乱的是现代Web开发,特别是那些某一天出现然后第二天消失的JavaScript框架。你需要三个脑袋才能跟上它。和我一起工作的JavaScript程序员也就够跟上一个(在这里是AngularJS)。

因此找一个能用JavaScript写iOS、Android和移动/桌面Web的人,使用现代的API并且仍然可以支持旧的OS版本,明白不同设计和UI方法的优缺点,特别是所有不同浏览器和Android设备中的微妙之处,并且在创纪录的时间内交付无bug的结果,是幻想。哦对,还要以低于市场的价格为你工作。

当我开始编程时,一切都极其原始,我只需要知道一门语言和一种OS,根本没有框架。甚至在我的两个创业公司里我只需要掌握C,通晓Macintosh和一些偶尔的68K汇编。今时不同以往。然而我们仍然只有一个大脑,并且大脑不服从摩尔定律,它们不能升级。

因此,如果你能(诚实地)同时做Android、iOS和移动Web,并且交付技艺精湛的结果,我向您致敬!但我真的希望你也能赚3倍的钱。

Elasticsearch的RESTful API

CURD的URL格式:

1
http://localhost:9200/<index>/<type>/[<id>]

id是可选的,不提供的话Elasticsearch会自动生成。index和type将信息进行分层,便于管理。可以将index理解为数据库,type理解为数据表。

创建

1
2
3
4
5
# 使用自动生成ID的方式新建纪录
curl -XPOST localhost:9200/<index>/<type> -d '{ "tag" : "bad" }'

# 使用指定的ID新建记录
curl -XPOST localhost:9200/<index>/<type>/3 -d '{ "tag" : "bad" }'

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查询所有的index和type的记录
curl -XGET localhost:9200/_search?pretty

# 查询某个index下所有type的记录
curl -XGET localhost:9200/<index>/_search?pretty

# 查询某个index下某个type下所有的记录
curl -XGET localhost:9200/<index>/<type>/_search?pretty

# 使用参数查询所有的记录
curl -XGET localhost:9200/_search?q=tag:bad&pretty

# 使用参数查询某个index下的所有记录
curl -XGET localhost:9200/<index>/_search?q=tag:bad&pretty

# 使用参数查询某个index下某个type下所有的记录
curl -XGET localhost:9200/<index>/<type>/_search?q=tag:bad&pretty

# 使用JSON参数查询所有的记录,-d代表一个JSON格式的对象
curl -XGET localhost:9200/_search?pretty -d '{ "query" : { "term" : { "tag" : "bad" } } }'

# 使用JSON参数查询某个index下的所有记录
curl -XGET localhost:9200/<index>/_search?pretty -d '{ "query" : { "term" : { "tag" : "bad" } } }'

# 使用JSON参数查询某个index下某个type下所有的记录
curl -XGET localhost:9200/<index>/<type>/_search?pretty -d '{ "query" : { "term" : { "tag" : "bad" } } }'

更新

1
curl -XPUT localhost:9200/<index>/<type>/3 -d '{ "tag" : "good" }'

删除

1
curl -XDELETE localhost:9200/<index>

Elasticsearch安装

Elasticsearch是一款基于Lucene构建的开源分布式全文检索服务器。提供RESTful API,采用多shard的方式保证数据安全,提供自动resharding的功能,能够很轻松地进行大规模的横向扩展,以支撑PB级的结构化和非结构化海量数据的处理。

安装Java 1.7

1
2
3
4
5
6
mkdir /usr/java
cd /usr/java
wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/7u67-b01/jdk-7u67-linux-x64.rpm
rpm -ivh jdk-7u67-linux-x64.rpm
java -version
echo $JAVA_HOME

安装Elasticsearch 1.4.1

1
2
3
4
5
6
mkdir /usr/elasticsearch
cd /usr/elasticsearch
wget https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.4.1.tar.gz
tar -xvf elasticsearch-1.4.1.tar.gz
cd elasticsearch-1.4.1
./bin/elasticsearch

然后访问http://localhost:9200/?pretty就可以看到类似下面的返回:

1
2
3
4
5
6
7
8
9
10
{
  "status" : 200,
  "name" : "Powerpax",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "1.4.1",
    "lucene_version" : "4.10.2"
  },
  "tagline" : "You Know, for Search"
}

只是运行起来是不够的,通常我们需要将Elasticsearch安装成服务,设置成开机自启动什么的。这要用到elasticsearch-servicewrapper了。下载解压后把service文件夹拷贝到elasticsearch-1.4.1/bin目录下。

1
2
./bin/service/elasticsearch install  # 安装服务
./bin/service/elasticsearch start  # 运行服务

其它选项:

1
2
3
console 以前台方式运行Elasticsearch
stop 停止Elasticsearch
remove 移除系统启动中的Elasticsearch服务(init.d/service)

需要注意的是,在小内存机器上运行时,需要限制下内存大小,否则服务会无法启动,出现如下警告信息:

1
2
3
Starting Elasticsearch...
Waiting for Elasticsearch......................
WARNING: Elasticsearch may have failed to start.

打开bin/service/elasticsearch.conf文件,设置Elasticsearch能够分配的JVM内存大小。一般情况下,设置成总内存的50%比较好。

1
set.default.ES_HEAP_SIZE=512

如果要限制ES_MIN_MEM和ES_MAX_MEM,建议设置成一样大,避免出现频繁的内存分配。

为什么我们不能用估算房屋同样的方法估算软件项目

英文原文:http://www.summa-tech.com/blog/2009/01/28/why-we-cant-estimate-software-project-the-same-way-we-estimate-houses

把构造软件类比成建造房屋是非常有用的,但也有缺陷。

尽管软件建设和住宅建设都是工程实践,但比起软件,我们在估算建造房屋的成本和精力方面更成功。

词汇障碍

因为我们大多数人一生都住在房子里,我们开发了一套定义良好的、可以理解的、几乎通用的词汇来描述和讨论关于我们称之为家的地方。当被问及我们的房子是什么样子的,我们可以很容易地回答。当计划一所新房子时,我们可以极其肯定地讨论我们想要什么,并确信建筑师和工程师会明白我们谈论的,反之亦然。我们可能不明白水管设施和电气细节,但我们知道电源插座要放在哪里,知道房间的大小,知道要有多少车库门。

当我们谈论软件时,就不是那么有效了。有太多来自业务和技术方面的新术语,我们需要依赖于现实世界中的类比去解释他们。不仅发生在业务和技术人员之间,甚至还发生在业务人员和业务人员,技术人员和技术人员之间。我们不习惯于去描述软件需求因为所有软件的抽象,在估算时沟通经常受到噪音、误解、缺乏眼界的影响,增加了真正需要构建的不确定性。

物理

现实世界有非常强大和稳定的规则,比如重力,这些规则会在盖房子的时候施加约束。我们知道我们必须在构建二楼前先完成地下室。我们不能在地基完成后增加层的数量。在水管设施铺设后更改浴室的位置将会是非常非常昂贵的。

在软件项目中,我们生活在一个较少规则的世界里,就像《黑客帝国》。从技术上讲,我们可以在同一时间构造所有的应用层。它可以被设计成使用不同的数据库,运行在不同的服务器,或支持不同的语言。它可以通过浏览器、手机、或其它无线设备访问。选项几乎是无穷无尽的。

正是这种自由和灵活性,在过去的40年里驱动了软件的巨大的进步,但在同时,它也是无数软件项目失败的原因。至关重要的是需求要面对现实,让它们遵守一些基本规则,即使它们不受宇宙物理规律的约束。

程序

物理和几千年建设的结合已经带来了如何去建造一座房子的一套可靠的程序,虽然总是会有新的材料和改进的技术,核心概念都是相同的。油漆房子的过程几百年来几乎没有变化。

我们仍然处于软件工程的早期阶段。大量的“直觉”仍在估算软件时使用,实际情况是直觉还没有被证明非常有效。

材料和标准

只有数量有限的材料能被实际地用在建造房屋上。从技术上来讲,可能有成千上百的选项去建造一堵墙:夹板、混凝土、钢铁、沙子、玻璃,但在家具建材零售店里这些选项是非常有限的。涂料的类型,门窗的模型很多但有限。当购买一个水槽时,它很容易兼容已有的水管设施的几率非常高。电器有着相同的电压,灯泡也是兼容的。计划建造一所房子的一切都是兼容的,材料更是普及的,把不确定性降低到了非常小的水平,提高了估算的准确性。

软件行业确实有一些标准,但是它们处于层的最低水平,例如网络协议和文件系统。服务器和产品的集成仍处于布线阶段,XML和Web Service还有很长的路要走,在它们和建筑业达到相同级别的兼容性前,如果这是可能的。

各种各样连接到数据库和构建软件的选项和方法增加了复杂性,提高了每个人参与软件构建工作的学习曲线。我不是说所有这些选项都是不好的,但它确实给估算阶段增加了不确定性,所以产生了复杂性。

角色

每次我路过建筑工地,都会看到很多帽子,一些在积极工作,一些在等待时机采取行动。但最好的部分是我从来没有见过有人同时戴两顶帽子。角色界定的很明确,工人们专业从事于非常具体的领域。

在大型软件项目中也有一些角色被定义,但还远远没有达到建筑业相同级别的专业化。通常团队成员需要戴上很多帽子,结果是,有时候他们会执行那些他们不是专家的任务,这就增加了他们提供的估算的不确定性。

“很多帽子”现象的一个很好例子是“Webmaster”,该角色用于描述那个做网页设计、创建动画图片、编写HTML和Perl代码、配置数据库、管理网络和邮件服务器的家伙。幸运的是现在Webmaster是个很少使用的术语,因为所有这些活动现在都分配给了不同的角色,像网页设计师、DBA,程序员和系统管理员。我们确实在走向专业化,但还是有很长的路要走。

我并不是建议我们停止使用“让我们像建造房屋那样构造软件”的类比,但我们必须意识到这个比喻的局限性。一旦我们知道局限性我们将能更好地定位讨论这一差异,以及提供建议如何解决它们。

TabActivity is deprecated

最近在整理Android Tab导航总结的代码时发现 TabActivity在API 13中被标记为过期了,所以就去寻找它的替换类,能尽量满足最小修改的要求。发现可以使用FragmentActivity来替代,Fragment组件作为标签页添加。

Fragment是Android 3.0引入的一个新概念,主要是为了适应各种不同的屏幕大小,它非常类似于Activity,可以像Activity一样包含布局,但是不能单独存在,只能存在于Activity中。下图是Fragment在不同屏幕上的显示以及Fragment与所在Activity的关系:

重构前的布局:

1
2
3
4
5
6
7
8
MainActivity extends TabActivity
    TabHost - tabhost
        LinearLayout
            TabWidget - tabs
            FrameLayout - tabcontent
                TabSpec (Activity)
                ...
                TabSpec (Activity)

重构后的布局:

1
2
3
4
5
6
7
8
MainActivity extends FragmentActivity
    TabHost - tabhost
        LinearLayout
            TabWidget - tabs
            FrameLayout - tabcontent
                TabSpec (Fragment)
                ...
                TabSpec (Fragment)

把TabLeftActivity和TabRightActivity分别改成LeftFragment和RightFragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LeftFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.tab_left, container, false);

        // do something
        // 不能直接使用findViewById()方法,必须加上rootView前缀
        // 如果要引用当前绑定的Activity实例,使用getActivity()方法

        return rootView;
    }
}

然后在main.xml中的FrameLayout里添加两个Fragment组件:

1
2
3
4
5
6
7
8
9
<fragment android:name="com.example.fragments.LeftFragment"
    android:id="@+id/fragment_left"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<fragment android:name="com.example.fragments.RightFragment"
    android:id="@+id/fragment_right"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

把MainActivity改成从FragmentActivity继承。这里不能像TabActivity一样直接用getTabHost(),需要改成如下代码:

1
2
TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);
tabHost.setup();

到这边就已经完成了,其它tabHost.addTab的使用方式一模一样。

根据Exif时间信息归类照片

先要把Exif中的信息解析出来,得到其中的时间,有个exif的gem很不错。然后再根据时间创建目录,把照片移动到对应的目录中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'rubygems'
require 'exifr'
require 'fileutils'

root = ARGV[0] || Dir.pwd

Dir.foreach(root) do |file|
  next if File.extname(file) != '.jpg'

  obj = EXIFR::JPEG.new(file)
  date_time_original = obj.exif.date_time_original if obj.exif?
  next if date_time_original.nil?

  dir = date_time_original.year.to_s
  Dir.mkdir(dir) unless Dir.exist?(dir)
  FileUtils.move(file, dir)
end

重构Rails代码

在以前写的博文部署应用到Heroku时的问题里有这么一段话:

股票功能需要导入交割单文件,因为导入后的文本文件不再使用,可以把上传路径由public/uploads改为tmp,这样就避免了Heroku不能写文件的问题。

下面是那时候写的导入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class StocksController < ApplicationController
  UPLOADS_DIRECTORY = File.join("#{Rails.root}", "public/uploads")

  def import
    filename = upload_file(params[:file])
    lines = File.readlines("#{UPLOADS_DIRECTORY}/#{filename}")
    lines.each do |line|
      # do something
    end
    redirect_to stocks_url
  end

  protected
    def upload_file(file)
      if !file.original_filename.empty?
        @filename = get_file_name(file.original_filename)
        Dir.mkdir("#{UPLOADS_DIRECTORY}") unless Dir.exist?("#{UPLOADS_DIRECTORY}")
        File.open("#{UPLOADS_DIRECTORY}/#{@filename}", "wb") do |f|
          f.write(file.read)
        end
        return @filename
      end
    end

    def get_file_name(filename)
      if !filename.nil?
        # does not support chinese filename?
        Time.now.strftime("%Y%m%d%H%M%S") + '.txt'
      end
    end
end

这里的实现方法是先将上传文件保存到服务器上应用的public/uploads目录中,然后再读取和处理。

其实根本不需要写的这么复杂,因为导入的文件被读取一次后就不再使用了。所以在当时写代码的时候一直有这样的想法,如果能直接获得上传文件的数据,那么就不需要再另外去写保存和读取文件的代码了。

事实也是如此。通过表单提交的file数据会首先在服务器上形成临时文件,这时其实可以通过临时文件的路径来读取上传文件的数据。

根据该想法重构后的代码如下:

1
2
3
4
5
6
7
8
class StocksController < ApplicationController
  def import
    lines = File.readlines(params[:file].tempfile.to_path.to_s)
    lines.each do |line|
      # do something
    end
    redirect_to stocks_url
  end

重构后的代码果然清爽多了,不过还是有改进的空间。

作为控制器,controller只是负责接收request,并返回response。而具体的业务逻辑,则应该交由model去完成。下面是依照该理念再次重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StocksController < ApplicationController
  def import
    Stock.import(params[:file])
    redirect_to stocks_url
  end
end

class Stock < ActiveRecord::Base
  def self.import(file)
    lines = File.readlines(file.tempfile.to_path.to_s)
    lines.each do |line|
      # do something
    end
  end
end