fsword's blog

A blogging framework for hackers.

为什么说比特币是货币?

| Comments

「摘要:货币本质上是一种通用凭证,证明拥有某种用于交换的资源和服务。劣币还是良币就看是否很容易对这个凭证起到“替代”作用,比特币在这一点上可以完美的抵御“注水”行为,因此可以很好的充当交易凭证,成为实际上的货币」

和朋友聊比特币,常常会有人问:“中国政府、日本政府都宣称比特币不是货币,你凭什么说它是货币”,这个问题很有代表性,我们就来专门谈谈这个问题。

首先要问,政府是否能决定一个东西是货币呢?

很多人都知道,1948年下半年国民党政府所发行的金元券,这是政府公开要求的货币,然而结果如何?金元券很快变成废纸,反倒是当时并非代表官方的共产党政权发行的货币逐渐成为主流。可能有人认为这个和政权更迭有关,因为看起来也可以理解为是国民党政府的政治和军事失败导致了金元券的失败,但是这个理由不是很有力,因为国统区金元券被逐步抛弃的时候,上海这样的大城市还没有易手,民众也没有换用解放区政府发行的货币,而是采用了以物易物和黄金作为代替。

更典型的例子是西汉末年的王莽币,那是具有政府信用的货币,而且在“王莽谦恭下士”的一开始,并没有政权被推翻的担忧,然而政府的货币政策反而加速了经济的崩溃,也导致了货币和王莽政权自身的灭亡。

上述的例子其实是说明了一个一般性的观点——货币现象终究还是经济现象,它必然要经过市场的考验,而政府虽然可以规定什么是法币,但是其实不能决定什么是老百姓赖以生活的通用货币。

随心所欲的发行一个货币,政府做不到,那么,能不能随心所欲的打压一种货币呢?

这要分开看——如果你去纳税,肯定要用政府指定的法定货币,比如我,纳税当然是人民币,不过,我也有一些同事,手头的美金比较方便,他纳税还可以拿出美金来,临时兑换成人民币缴税,这些并不矛盾。所以,从纳税这个功能看,政府指定一种货币不等于排斥另一种货币。

从另一方面看,政府可以下禁令禁止持有或者交易某种物品的,比如,在1947年的上海,你拿着解放区政府的货币是不太安全的。在这个意义上,政府确实能够打压一种货币——但是,这种打压依然是有限的,想想黑市吧,这个隐藏的市场虽然无时无刻不受到各国政府的联合打压,然而依然在持续的运行。无论我们喜欢不喜欢,可以预想的将来它都不会消亡。

小结一下上面所说的内容吧,其实归根到底一句话,这是——“市场的力量”,经历过改革开放的人们,对这个概念应该是再熟悉不过了。好的货币被保留,不是因为政府支持,而是因为市场认可,坏的货币被淘汰,不是因为政府反对,关键还是市场的否定。

既然市场是关键,那么让我们回到货币本身,思考一下什么才是货币。

众所周知,人类使用的货币的过程是从贝壳到金属币(例如铜钱、银子和金条),再到纸币的。那么问题就来了,贝壳为什么会被淘汰?其它金属为什么没能成为通用货币?后来又怎么被一张小纸片所代替?

有人解释说:金属是有用的,而贝壳是无用的。这个解释很牵强,因为我们知道秦半两和汉代五铢钱都是铜币,而在那个年代,铁器正在以更坚牢锐利的特点慢慢走向生产生活的舞台中央,那么,更有用的铁为什么没有成为主要货币呢?

我们熟悉的黄金也是一个很好的例子,很多人认为黄金背后的价值来源于它“有用”,这其实是一个误解。黄金不像钻石那样有很好的工业用途,那灿烂的颜色在现代新材料面前也没有什么不可替代的优势,对现代人来说,黄金除了加工为艺术品,已经基本没有什么拿得出手的使用价值,而成为工艺品,其中一个很大原因还是因为它本身很贵,所以,甚至艺术品制造也不能成为黄金有用的理由。

有用并不是货币的关键属性,那么它的关键属性是什么呢?我想,至少应该有这么几条——易于切分、便于携带、不易被替代。

我们先说最简单的前两条:

一、易于切分:这个很容易理解,金属币正是在这一点上领先于贝壳等早期货币,现代纸币通过不同面值的组合,大致上也能达到这个要求。

二、便于携带:货币是用来交易的,因此是否便于携带就很重要了,金属币在这方面是劣于纸币的,所以现代社会的黄金大部分都在国家的央行金库中,而与纸币相比,电子货币是更为方便和易于携带的货币形态。

作为电子系统,比特币天然便于携带,同时它在设计上支持无限切分,所以在前两条上,它相比任何货币形态都不落下风,但是更大的关键在第三条,而第三条不但不易理解,甚至也不好描述——其实“替代”这个用词并不太准确,只是考虑了很久我也找不到更好的说法。不过我还可以举例来说明我的意思,一般来说,我所说的的“替代”有这么几种常见方式——

  • 伪钞:这是最容易理解的一种“替代”,由于大多数人不能分辨钞票的真伪,拿着一张制作精美的伪钞会和真钞票具有同样的购买效果,如果打击伪钞不力,那么伪钞就可以“替代”真钞

  • (政府)货币增发:根据法律,新印出来的货币和原来市场上的货币具有同样的法律效力,也就是说,新钱可以“替代”旧钱

  • (贵金属)发现新矿:例如地理大发现改变了黄金和白银在欧洲的总拥有量,大家都是金块,从美洲运回的和旧的是一样的,新黄金可以“替代”旧黄金

上述情况虽然看似不同,但是对货币而言其效果却是相同的——钱不值钱了。

为什么要讨论“替代”,我们先来看看货币的价值。用户持有货币,目的是期望这玩意能够被换成自己需要的东西(也许是马上,也许是未来)。而对普通人来说,获取货币的方式是交换——俗称挣钱——纸币也好,黄金也罢,我们得到它依靠的是付出某种商品或者服务,比如我卖一个苹果能获得两块钱,这个行为背后是我的一次价值判断——一个苹果价值为一块钱。

货币其实是某种凭证,我们以此证明自己曾经付出过劳动,出让过货物,提供过帮助,接受这个货币实际上是在与这些劳动、货物、帮助背后蕴含的价值进行交换。而一个凭证,其本身最重要的特性是什么呢?那就是不可替代!

举个例子,我向别人借本书,同时写了个借条给对方,对我来说,这个借条是用纸做的(纸币)还是用铜做的(铜币)并不重要,重要的是我要根据这个借条还书,所以这个借条一定是“独一份”,如果谁都能自己写个纸条,然后让我把书交还给他,那可就糟糕了。

现在回到“替代”这个概念,无论是上面举的哪个例子,货币的总量都变多了——当初我借给你的书得到一张两块钱的借条,而现在有人直接开一下印钞机就打印出了我们无法分辨的借条,那谁还愿意把书借给你?

“替代”这个概念很重要,因为它是我们理解通胀现象的基础,简言之,如果我们能区别两个货币,那么它们不存在“替代”的可能,而是会形成汇率,除非我们规定一个不变的汇率,否则一种货币的通胀不会传递到另外一种货币。

很多名气很大(我不知道实际水平如何)的人也犯过这个错误,比如一些人喜欢的郎咸平先生,他曾在节目中提出过一个观点:“比特币总量虽然不变,但是我们可以创造出各种山寨币,种类的增加还是会通货膨胀”,这话说的简直就像个外行,因为山寨币并不能和比特币互相替代,欧盟统一了货币,也没见因此而通缩。

有人会有疑问,现实生活中,也有利用外国货币冲击一国经济的情况,如果不能“替代”就不会引入通胀,那么这种现象又是怎么回事呢?

很简单,增加货币种类不会直接导致通胀,但是外国货币可能会对应到一些资源,由于本来由本国法币对应的资源被改用外国货币对应,本国的货币就显得多余了,钱多货少,于是价格就下跌了。那么郎咸平说的对吗?还是不对,因为这种“货币竞争”是要看货币的强弱的,如果中国放开货币限制,允许美元长驱直入,可能会有一些资源改为由美元对应,但是如果是是越南盾进来,则不会有这样的现象,因为人们相信美元,不相信越南盾,所以根本就不会有什么资产使用越南盾对应。

无论这个货币使用什么方式衡量多寡(比如黄金按成色和重量,纸币按照面值等等),他被伪造就是一种被替代——因为在一定场合下,假钞是可以代替真钞票进行交易的,此时,真钞票就被“替代”了。

通货膨胀也是一种替代,所谓钞票,每一张之间应当是含义相同的,除了特别设计的不同面值,使用中应该是一样的,而如果增加钞票的发行量,虽然发行的是真钞票,但是对经济的危害和伪钞是一样的——都会使得原来付出代价获得的钞票变得不那么稀罕。

理解了替代的概念,我们再来看看现有的几种被当作货币的事物,拿它们来与比特币做一个对比吧。

  • 黄金:出现新的资源几乎不太可能,所以没有“替代”。但是另一方面,黄金可以“以次充好”,如果要避免被“替代”,我们还需要能够鉴定成色,这是使用它的一大麻烦。

  • 各国法币:是否存在新的替代,完全由央行决定,因此是否存在“替代”,要看政府的财政政策。另外,根据纸币的制作工艺,不同的法币还有不同比例的伪钞在市面上流通,它们也是“替代”。

  • 比特币:由算法决定没有人能增加真的货币数量,无可替代。同时,由于它本质上是向全世界公开整个帐簿,因此也无法伪造。

本身就易于切分和携带,再加上没有发生替代的可能,比特币无疑是更有资格成为货币的。即使各国央行联合抵制,也只是延缓这一进程,因为它其实是适应市场的需要而生,而最终的选择是市场作出的。

一个bug的修改

| Comments

下面是一段工作中编写的代码,为便于理解修改如下——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Order
  attr_accessor :type, :sub_orders

  # 统计订单及其包含子订单的数量,按照订单类型归类
  def count_items
    result = sub_orders.reduce({}) do |result, order|
      order.count_items.each do |key, value|
        result[key] = (result[key]||0) + value
      end
    end
    result[type] = (result[type]||0) + 1
    result
  end
end

乍一看没问题,但是计算出来的数字始终不对,细看才发现问题,修改一下:

1
2
3
4
5
6
7
8
9
10
  def count_items
    result = sub_orders.reduce({}) do |result, order|
      order.count_items.each do |key, value|
        result[key] = (result[key]||0) + value
      end
      result #添加这一行
    end
    result[type] = (result[type]||0) + 1
    result
  end

分析原因,是对reduce的使用不够细致:each和map容易和以前写java代码时的思维方式一致,所以不容易犯错;而reduce的返回值用于进一步迭代,这种做法以前用的较少,潜意识里还是将result和s看作是一个服务于循环的变量。

想明白以后再带着新的角度看这个代码,于是发现还可以简化,reduce操作的最大特点就是将初始值和结果纳入到计算框架中,这样可以减少很多重复劳动,最后改成这样

1
2
3
4
5
6
7
  def count_items
    sub_orders.reduce({ type => 1 }) do |result, order|
      order.count_items.reduce(result) do |s, (key, value)|
        s.update key => (s[key]||0) + value
      end
    end
  end

其实就两点:1. block参数的模式匹配;2. update的返回值就是当前对象

试验代码在 这里 ,Be fun!

分享笔记-服务设计

| Comments

这是公司UI团队的分享,做一个简单的摘要

服务体验的相关要素

  • 空间
  • (周边)物体
  • 界面(包括交互方式)
  • 传播
  • 衡量

服务体验分析

  • 分解服务环节
  • 对每一个服务环节进行分析,分析思路可以是——
1
2
3
4
5
 -> 参与体验者
   -> 提供的服务
     -> 体验感走势 
       -> 用户的担忧 
         -> 增加的服务

举例

  • 拔牙的手势: 提供给用户明确的感情交流机会
  • 51job -> linkedin:产品定位的变化

其它

和客户沟通的时候需要有一定的方法,避免开放式的讨论

单测与持续集成3-erlang例子

| Comments

本篇是这篇的后续,很早就放进了草稿箱,但是我一直懒得修改好,真是典型的拖延症患者。

我自己在项目中使用erlang时间并不长,而且断断续续,充其量是个初学者。之所以用erlang举例子,是因为它比较有代表性。

学习erlang,OTP是个转折点,接触了gen_server等一系列模式以后,很自然就会感觉到其实一个erlang进程更像是一个对象——有标识符,内部保存状态、对外提供服务接口、可用的交互通过消息传递进行等等。

相应的,进行测试时的问题也很类似。人们在宣传erlang时常常说它由于状态不可变,所以可测性很好,然而如果以一个进程为测试对象来看,我们会遇到进程间协作的问题——这相当于java程序里面的对象间协作。

在apposs_agent项目中我就遇到了这类问题,例如,有三个模块之间的依赖关系如下:

client -> responder
       -> ssh_executor

那么,这时模块client中怎么使用其它模块呢?下意识的,我们让这种依赖变得“可插入”,于是就有了类似这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%% client.erl
%% .....
init([Responder_mod, Host, GetHostInfoFun]) ->
  %% ...
  State = #state{host = Host,
                 get_host_info_fun=GetHostInfoFun,
                 responder_mod = Responder_mod,
                 cmds = Cmds
                },
  gen_fsm:send_all_state_event(?SERVER(Host), reconnect),
  {ok, disconnected, State}.

%% ......
normal(do_cmd, #state{host=Host, cm=Cm, cmds=[Cmd|T_cmds], exec_mod=ExecMod, responder_mod=RespMod}=State) ->
  (RespMod:run_caller(client))(Host, Cmd),
  Handler = ExecMod:exec(Cm, Cmd),
  {next_state, run, State#state{current_cmd=Cmd, cmds=T_cmds, handler=Handler}};
%% ......

在state中放入复杂的数据结构,其实就是为了让ExecMod和RespMod变得可以“配置”,还单独测试client而不用依赖其它模块。

然而这种设计的成本实在太高了,实际中,exec_mod和responder_mod并不会改变,这个目标属于over design。

那么为了可测性是否有必要这么做呢?有一段时间我也不是很确定。

从结构上来看,ssh_executor和responder属于下层模块,client是它们的用户,隔离下层模块运行上层模块意义不大。从某种角度看,软件开发很像搭积木,我们做好下层模块以后就不用“隔离”它们了,要测试,带上大家一起跑是很方便的做法。

想通了这一点,上述的代码就很简单了——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
init([Host]) ->
  %% ...
  State = #state{host = Host,
                 cmds = Cmds
                },
  gen_fsm:send_all_state_event(?SERVER(Host), reconnect),
  {ok, disconnected, State}.

%% ......
normal(do_cmd, #state{host=Host, cm=Cm, cmds=[Cmd|T_cmds]}=State) ->
  (responder:run_caller(client))(Host, Cmd),
  Handler = ssh_executor:exec(Cm, Cmd),
  {next_state, run, State#state{current_cmd=Cmd, cmds=T_cmds, handler=Handler}};
%% ......

显然,这样修改以后,不但代码量减少了,而且代码专注于表达业务逻辑,因而也更容易理解了,好的代码应该专注,而不是眉毛胡子一把抓,您说是不是?

有人会说,那我们难道就不要单元测试了么?不错,这正是我想说的,单元测试的关注点应该是具有很高算法复杂度的逻辑单元,而不是复杂性都委托出去的业务模块,对于后者,不做单元测试并不是罪过。

上述的想法也只是逻辑推演,为了更加确认,我和淘宝内部的一个erlang项目的同事做了一些了解——

1
2
3
4
5
6
hi,你们的代码写很多mock吗?“
”写的不多“
”那单元测试的时候怎么隔离呢?要在进程内部的状态里面保存关联模块么?”
“不用,我们主要写集成测试”
“集成测试会不会覆盖不到位?”
“目前的功能以业务为主,逻辑不是很复杂,用集成测试就够了”

同事的回答和我想的一样,不过这毕竟只是少数项目,我希望能有更多的案例,大家一起交流、讨论

Require使用例子

| Comments

常常有人搞不清楚ruby脚本怎么装载,其实ruby有一个$LOAD_PATH全局变量,相当于java的classpath,require就是从这里的目录中进行查找的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ls
boot.rb
$ irb
irb(main):001:0> require 'boot'
LoadError: cannot load such file -- boot
    from /home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
    from /home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
    from (irb):1
    from /home/john/.rvm/rubies/ruby-1.9.3-p392/bin/irb:13:in `<main>'
irb(main):002:0> puts $LOAD_PATH
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1/x86_64-linux
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby/1.9.1
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby/1.9.1/x86_64-linux
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/1.9.1
/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/1.9.1/x86_64-linux
=> nil
irb(main):003:0> $LOAD_PATH << '.'
=> ["/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby/1.9.1/x86_64-linux", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/site_ruby", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby/1.9.1", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby/1.9.1/x86_64-linux", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/vendor_ruby", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/1.9.1", "/home/john/.rvm/rubies/ruby-1.9.3-p392/lib/ruby/1.9.1/x86_64-linux", "."]
irb(main):004:0> require 'boot'
=> true

单测与持续集成2-项目中的例子

| Comments

(原来的标题太长,改短一些 :-) )

前一篇博客写完以后觉得有些空,这篇说说现实中的例子。

淘宝是java集散地,我们以java项目为例,下面是一个项目中的测试代码——

1
2
3
4
5
6
7
@Test
public void addCreative() {
    CreativeDO creative = new CreativeDO();
    ...此处省略11set语句...
    long creativeId = this.creativeDAO.addCreative(creative);
    Assert.assertTrue(creativeId > 0);
}

这个test方法只是用了最基本的验证方式,显然这是用于确保基本功能——能够正确的添加记录,充其量只是用来验证和数据库相关的配置文件是否正确

我们继续看上层代码——

1
2
3
4
5
6
7
8
public class CreativeServiceImpl implements CreativeService{
    ...
    public ResultDTO<Long> addCreative(CreativeDTO creativeDTO) {
        ...
        creativeDAO.addCreative(do);
        ...
    }
}

这个类有自己的测试保护,不过这次我们就不要看那些代码了,先用vim看看outline吧——

1
2
3
4
5
6
7
8
9
10
|-   class
||     CreativeServiceTest
|-   field
||     creativeService [CreativeServiceTest]
||     creativeDAO [CreativeServiceTest]
||     ...
|-   method
||     before [CreativeServiceTest]
||     ...此处省略10个用于测试addCreative的用例...
||     addCreative [CreativeServiceTest]

显然,服务层方法有非常充分的测试保护,通过服务层调用,creativeDTO也间接得到了验证,其功能覆盖要远大于之前单独针对DAO的测试用例。

那么,我们为什么要写那个DTO的测试呢?不但费力(11行set,每一行都需要精心填入合理的数据),还用处不大。

出现这个现象一般有两个原因——
* 实践中,工程师有时是先写DAO再写上层模块,这样,写完DAO以后service还不存在,要验证是正确性就必须写DAO的test case。 * service的运行一般是依赖某些服务容器的,为它编写的test case相当于服务器外部的一个客户端,而这需要将service部署起来。

前一个原因还好办,很多java团队都意识到DAO和相邻的软件层之间存在某些重复性,因而可以用类似代码模板的方式一次性生成DAO、Service。这时,service和DAO是一起出现的,这样,我们就可以直接关心service了。

而后一个原因则是困难所在,因为在没有持续集成意识并建立持续集成机制的团队中,“部署应用”这件事是所有工作中相对靠后的一步,通常是开发时间过半以后才有第一次部署,而这时团队成员们已经重复的写了很多测试用例。

解决这个问题,只有引入持续集成,在系统一开始,我们就“一杆子捅到底”,从“白板应用”开始进行持续构建、部署,新代码和功能不断地交付,功能验证可以在适合的层次上进行,对上面的例子而言,我们直接写service测试,那么对DAO部分的验证就可以直接省略了。

这一篇说的是java的场景,但其实道理有共同性,下次我们讨论一下erlang项目中的测试

单元测试不是持续集成的基础

| Comments

很多人关注甚至想尝试持续集成,然而也有一些人担心团队缺乏基础——“我们连单元测试都做不好,做持续集成不太合适吧”。如果不会走就学跑,那确实容易摔跤。不过,单元测试和持续集成并不是走和跑的关系。

为避免误解,首先明确一下名词(虽然没有照抄书本,但是应该不会差太远吧):

  • 单元测试:以验证某个代码单元的正确性为目标进行的自动化测试活动,“代码单元”通常是函数、方法或者是类,测试过程中,目标单元对外部的编译或者功能依赖由stub或者mock技术进行隔离。
  • 持续集成:一种敏捷实践,重点是尽早进行系统的集成测试,它在狭义上包括部署自动化和测试自动化。“持续”一般被理解为不断的对研发变更进行整体验证,“集成”通常包括对分支的集成(因此一般推荐单分支开发)和在一定条件下对不同子系统或者模块进行的集成。

可以看出,单元测试针对的目标是局部而非整体,而持续集成面对的是整体。按照“饭要一口一口吃”的老话,似乎应该先做单元测试。

然而单元测试并不是免费的。任何自动化测试都是基于测试目标的功能而实现,因此,测试目标的稳定性就变成了测试价值的一个重要因素——时常发生变化的代码,对其做自动化测试是不划算的。那么,单元测试所针对那些小粒度的类、方法和函数,它们变化剧烈吗?

这可能和软件系统的类型有关。例如,如果我们是在开发一个短信发送客户端,由于所遵循的SMGP协议本身是相对稳定的,网络相关的功能单元就是稳定的;然而,如果我们开发一个应用系统(比如各种大大小小的互联网应用),业务上的变化可能对下层的模型代码产生天翻覆地的影响。考虑到大部分的软件研发团队和研发工程师们所处理的都是基于数据库+web的应用系统,我们所遇到的场景很可能是后一种情况。

去年我所在的团队推进质量改进时我们就发现了这个规律,当时我们首先推进的就是单元测试,虽然我强调“自动化测试”而非单元测试,但是开发同事们都很自然把精力放到了单元测试上。在一段时间的热心实施以后,一些人开始出现不同的声音——“有些测试刚写好,业务就发生了变化,不得不完全抛弃,瞎耽误时间”。问题显然不是同事们不尽责,我们分析发现,因为单元测试用例过于关注细节,业务变化的情况下很难进行积累,再继续下去会出现“边际效益递减”的情况,而如果开始做持续集成方面的工作,则可以补充自动化的集成用例——它相对稳定。

除此之外,单元测试还有一个常见的问题:mock的代价。

几年前ThoughtWorks的李晓有过一篇不要把Mock当作你的设计利器这里还有gigix转述郭晓的观点——

I did have some doubts about using Mocks when i was programming, similar 
reasons - too hard to refactory, too brittle. And i total agree with the 
three places to use it - external resources (I/O), UI, third party API.

也许有人觉得这里的观点有些“极端”(好像中国人对“极端”是比较敏感的 :-D ),然而我们在实际工作中很容易感受到上述文章和引论所说的痛点。这里存在两个方面的问题——

  • 对变化不友好:一旦我们进行了mock,就在事实上建立了对外部变化的“屏障”——每次发生变化时都有可能忘记了被mock掉的“结合点”,即使记得,也增加了重构的成本,时间一长,维护mock代码就变成了一件苦差事
  • 推迟集成:有了mock以后,我们可以很容易就建立起自动化验证机制。但是错误往往在于疏忽——mock掉的那个东西,未来需要使用“真实的东西“再测一遍,这不止增加了测试成本,而且还会在前期给人以“系统没问题”的错觉

顺便说一句,这些问题在stub中也是类似的,mock和stub还有一些差异,但是这里就不涉及了。

对于这些分析,路宁同学 的一个简单易用的观点是:“不对自己开发的模块写mock”,这个很好理解,因为自己开发的模块可以直接用“真家伙”,那么“假李鬼”也就用不着了。

我们是否可以沿着这个思路继续推进呢?实际上,之所以要区分“自己的模块”,是因为“自己的模块”好合作(自己和自己当然好合作),那么在我们推进持续集成以后呢?

持续集成,表面上看是在做部署自动化和测试自动化。然而这个实践的一个重要价值是“弥合缺口”——通过持续的将版本控制系统的多个分支合并到一个分支上,避免了分之间的鸿沟越来越大;通过持续的将系统的各个部分完整的部署在一起进行自动化联调和系统测试,避免了子系统与子系统、模块与模块之间的衔接隔阂。

前者好理解,后者一般容易被忽视,我们知道,某些语言特性和框架也试图解决这种系统和模块边界
的衔接问题,例如java的interface,它就是设计来建立系统间协作接口的。然而真实的世界很难用
interface这类技术进行约束,即使实现了同样的interface,我们也不确定边界两边都遵守共同的约
束,能让我们放心的只有联调和系统测试。

显然,在我们推进持续集成的工作并通过这个工作不断的“弥合缺口”以后,那些之前不得不mock掉的所谓“别人的模块”甚至“外部的子系统”也就不再变得遥不可及而难以合作了。于是,我们惊喜的发现——mock变得可以省略了,随着持续集成的推进,一些原来不得不编写的mock可以直接用“真家伙”代替,而原来所倚仗的单元测试用例也随之变成了集成用例、联调用例……

所以,单元测试并不是集成测试的基础。实际上,往往是持续集成扩展了质量保障的手段和方式,并因此减弱了单元测试的压力,从此我们可以专注在必要的单元测试用例上了。

如果你的团队做单元测试不是很给力,可以先找找原因,如果不是大家的主观意愿问题,不妨和持续集成的工作一起推进吧

持续集成中的缺环-4

| Comments

昨天我用了很罗嗦的语言解释了一个简单的 DSL 例子,这是为了方便讲述后面的内容,那个例子是典型的“麻雀虽小,五脏俱全”,回想起来,它至少包含了这么几样——

* 一个 DSL 解释脚本(dsl.rb)
* 一个装载脚本(run.rb)
* 使用 DSL 编写的文件(myapp)

这么继续演化下去,我们就会得到一个比较详细的实现方案。

不过,考虑到复杂性的增加,我们现在应该考虑一点设计了,比如代码的层次和结构。

代码如何组织呢?这是一个脚本为中心的项目,因此我们决定不使用unix那种传统的/usr, /bin, /etc 方案,而是直接把所有代码都放在一个目录下,这样比较方便清理,不用专门的打包工具。

首先很容易想到,我们需要普通的 config 目录;其次,考虑到我们会引入很多 profile 文件,所以建立一个专门的profile目录似乎也是很必要的;另外,作为脚本性质的项目,一定要有明确的调用入口,我们建立一个 script 目录用于存放直接执行用的脚本。

接下来是 lib 目录,我们应该把代码分开,做好高內聚和低耦合(这个没忘吧,脚本也要注意这些原则哦),因此一开始会是这样——

目录结构

看起来不错,但是用代码稍一尝试就发现了一些问题,主要是lib目录。

  • 没有考虑到外部服务的耦合,我们使用的系统服务应该是松耦合在这个体系中的,比如虚拟机分配系统,在淘宝这样的环境中,虚拟机分配系统可能有多种方案,随着业务和组织结构的变化,我们有可能需要切换不同的分配系统。
  • 应用专用的目录没有办法去落实,实际上,DSL的方案决定了我们不会为某个应用做特殊化,我们要做的是各种服务的组装。
  • 与capistrano相比,profile的设计显然有些僵硬,后续可能需要调整,当然,这样演化出来的可能是一个工具包,但是目前也想不清楚,我们把决策延后,暂时先不管它。

经过设计和编码的迭代,目前是这样的:

目录结构

这并不是最后的结论,大家知道,设计和开发往往是交替进行的。不过无论如何,现在可以接着前进了

持续集成中的缺环-3

| Comments

终于要讲到 DSL 了,其实这个话题我更加感兴趣一些,这也是ruby的强项 :-)

我们首先应该从用户角度出发,看看用户写出来的 dsl 应该是什么样子,例如像一个配置文件:

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
32
33
# -*- encoding : UTF-8 -*-

# 定义web server类型,可选值: apache, nginx, tengine
# web_server :apache
web_server :apache

# 定义应用服务器类型,可选值: tomcat, jboss
# app_server :tomcat # option: jboss

# 申请数据库资源
# from: 有时需要从已有的数据库中复制数据,from参数用于指明来源的数据库名称
db :from => 'sample'

# 设定源代码获取方式,目前仅支持git和svn两种
source :svn => 'http://svn.yourserver.com/branches/some_branch'

# 设定安装包的来源,目前仅支持 rpm 包安装
# 注意:如果设定了安装包,adsci将跳过build环境,此时source设定不会生效
pkg   :rpm => 'http://yum.yourserver.com/your_package.rpm'

# 设定 mock 服务,指定的服务将用 mock 支持
mock :service => ['myservice.core.1.0.0']

# 设定所依赖的其它 profile ,需要在指定 profile 的服务/应用启动后再启动
after :portal  do
  # 在指定的 profile (这个例子中是 portal 应用)中为当前节点添加 url rewrite 规则
  rewrite :for => :me
end

# 设定依赖的其它 profile ,不考虑启动顺序
after :portal  do
  route :for => :me
end

真正的 DSL 应该比这个更强大一些,不过从这样开始应该不错。

硬广告:这个文件比较简单,唯一需要说明的是tengine,这是淘宝基于nginx扩展的一个开源项目,好事者可以看这里

用户通过这种方式说明自己的应用应该是怎样部署和工作的,其它事情交给工具。

实现DSL最常见的做法就是把DSL语句变成一个函数,然后在定义函数的上下文中eval用户的脚本,比如一个最简单的语法web_server :apache, 可以这么写——

1
2
3
4
5
module Dsl
  def web_server name
    puts "设定web server为#{name}"
  end
end

执行结果——

1
2
3
4
5
1.9.3p327 :008 > include Dsl
 => Object 
1.9.3p327 :009 > eval("web_server :nginx")
设定web server为nginx
 => nil 

这就是最简单的配置项。
但是这样做有什么意义呢?我们可以这样修改一下 Dsl 的代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- encoding : UTF-8 -*-
# 文件名 dsl.rb
module Dsl
  def web_server name
    if name == :apache
      `sudo /etc/init.d/httpd start`
    elsif name == :nginx
      `sudo /etc/init.d/nginx start`
    else
      puts '抱歉,目前不支持这种 web server'
      return
    end
    puts "启动 #{name}"
  end
end

同样也把eval的内容存为文件

1
2
3
# -*- encoding : UTF-8 -*-
# 文件名 myapp
web_server :nginx

最后编写一个装载脚本

1
2
3
4
5
6
7
# -*- encoding : UTF-8 -*-
#!/usr/bin/env ruby
# 文件名 run.rb
require './dsl'

include Dsl
eval File.read('myapp')

执行一下:

1
2
3
$ ruby run.rb
[sudo] password for john:
启动 nginx

这样就完成了一个最简单的 dsl 样例,在这个样例中,我们通过一个关键词 web_server 驱动了逻辑

持续集成中的缺环-2

| Comments

「接上篇」比较新旧两个图,可以发现,除了要做好多应用的部署自动化以外,还有一个问题需要我们考虑,那就是资源的自动分配。

常见的是数据库资源,由于我们过去一直依赖DBA分配数据库帐号,每次测试都是使用的同样的一个数据库,这样一来,开发人员想要测试就要申请自己的数据库资源,有新来的同学如果不知道,就会用源码中配好的那个数据库链接,如果这时有人在测试,那就悲剧了。而如果偶尔需要多个分支一起开发,情况就更加可怕。

解决这个问题的办法是资源隔离和动态分配。而且,这个做法不只限于数据库,几乎所有的基础设施都要这么做,你会发现这个环境似乎有些象“云”,好吧,虽然我不喜欢这个buzz word,但是沿着这个思路走下去,确实可以用到一些“云xx”的技术,我们只要拿来主义就可以了。

循着这个思路,我们搭建了一些支持动态分配资源的服务,这是后续工作的基础。为了能够方便的操控部署工作,我又把自己一直负责的easy commander(以前叫 AppOSS)也嫁接了过来,这样我们就得到了这些

部署图

对单个应用而言,本来就有一些shell脚本(例如执行 maven 打包和启动停止服务器之类),所以我只要简单写一些ruby脚本负责协作,就让这个原型跑了起来,经过验证,可行。

接下来要做什么?我们现在仅仅用脚本驱动了起来,而ruby语言在我们这个团队中并不是公共知识,所以最好能够开发一些 DSL ,让大家简单的写一些“配置文件”,就能让自己关心的几个系统部署好。

如何实现这些DSL呢?明天接着说