逸言

使用扩展方法实现重用

| Comments

我们已经有了一个IRepository接口,它提供了诸如BatchSave(),Insert()之类的方法。其中,BatchSave()方法接收一个由实体类组成的数组,从而完成对实体对象的批量存储。

对于单个实体对象而言,我们当然也需要有相关的方法来完成存储。它与BatchSave()的区别仅在于它要接收的参数只是一个实体对象。事实上,我们可以调用IRepository接口本身提供的BatchSave()来实现Save()方法。Save()方法的实现对于所有实现了IRepository接口的类而言,完全是一样的。那么,我们该如何为IRepository接口增加一个Save()方法,同时又能避免代码做出太多修改?由于接口自身并不能提供实现,因此唯一可以容纳这个方法实现的就是实现了IRepository接口的类。一个办法是为所有这些实现类定义一个公共的抽象父类,并让它再实现IRepository接口,于是将Save()的实现放到这个抽象父类中,就可以使得所有子类共享Save()方法的实现了。可是,为了这个Save()方法的实现与重用,专门引入一个抽象父类,是否值得呢?除非在现有的Repository继承体系中,已经存在了这样的抽象父类,否则该类的引入会导致整个继承体系发生较大的变化。

在.NET中,我们可以巧妙地借助扩展方法来完成对Save()方法的重用,同时又能保证现有的继承体系不变。方法就是为IRepository接口提供扩展方法:

public static class RepositoryExtensions {
  public static void Save<T>(this IRepository<T> repository, T entity) {
      repo.BatchSave(new[] {entity});
  }
}

处理文件请求限制

| Comments

在我参与的一个项目中,遇见了一个结合功能性需求与非功能性需求,并要求同时满足的场景。它的功能其实很简单,就是需要向系统发出处理文件的请求。文件的处理则涉及到多个数据表的查询,对相关数据的解析,并依照事先设定好的模板填充数据,最后生成PDF文件。一旦文件处理完毕,就可以返回处理后的文件。由于该系统的业务特殊性,这一功能需求会在某个特定时间,迎来数以万计的客户请求。同时,文件处理功能是一个相对漫长的处理过程,且生成的文件较大。在系统的最初版本中,经历过数千人次的并发数,在只有一台服务器的情况下,导致了大量请求的阻塞。同时,由于加载文件和文件读写需要耗费内存,在请求较为频繁的情况下,多次抛出OutOfMemory异常。即使在最好的情况下,服务端响应了客户端请求,也可能花费大量的时间,严重影响了用户体验。

我们希望在后续版本中解决这一问题。然而,现实总是这么残酷。真正处理文件并提供下载功能的系统并不在我们的掌控之中。它是第三方Vendor提供的Web Service,我们开发的系统仅仅涉及到请求的转发,完成对该Web Service的调用;并在获得结果后,将响应(包含了文件流数据)返回给客户端。换言之,我们既不能改善文件处理的实现逻辑,以提高处理的速度;也无法对该Web Service进行水平伸缩,例如通过引入多台服务器建立集群和负载均衡的方式。

遭遇如此场景实属无奈,要得出好的设计决策就好似戴着镣铐跳舞,只有在自己的服务端下功夫。我们首先想到的是限流(throttle)的方式,通过引入一个类似Controller角色的对象RequestHandlerPool,对客户端的请求进行控制。我们可以设定一个阈值,一旦超过该阈值,就将后续的请求放入队列进行排队。这个限流可以采用简单地在内存实现请求池全局对象。当然,也可以考虑引入消息队列中间件。改进后的时序图如下所示:

引入RequestHandlerPool仅仅是对请求进行了限制,从而避免请求过多导致File Cabinet的阻塞,或者导致抛出OutOfMemeory异常。但整体的处理时间并没有得到任何改善。我们首先考虑将该功能分为两阶段。第一阶段是发起对文件的处理请求,第二阶段则是下载处理好的文件。对于耗时较长的文件处理请求,可以考虑使用异步请求,一旦文件处理完毕,就可以通过Callback通知请求者。然而,由于文件处理的时间过长,可能会导致请求者不愿继续等待结果,从而退出系统,形成一次失败的请求。因而,我们考虑系统的Callback可以通过发送邮件的方式通知发出请求的客户,在邮件内容中附带下载地址,以供客户下载。

纵观整个场景,存在太多制肘,我们也没有太多好的解决方案。而且,我们还应该保证这个解决方案足够简单,因为我们需要在尽量短的周期内对原有方案进行改善,以迎接新一期的业务高峰。这些限制不同于架构约束,它常常迫使我们在逼仄的空间中闪转腾挪。我们还必须尽快地实现方案的原型,并营造与真实业务场景相当的数据,对其进行压力测试和性能测试。

ThoughtWorks读书雷达-架构设计篇

| Comments

编码的能力与设计的能力,二者均不可偏废。我们认为,编码事实上是设计的一部分,只不过它更多的是以代码的形式来呈现,而设计则主要由模型来组成。这种设计模型有助于知识的传递与分享,同时也可以有效地提高编码质量。至于架构,则是软件系统中重要的事物,它关乎的内容往往是难以更改的。卓越的软件设计一定拥有美的架构,也能够有助于促进架构的演化。在ThoughtWorks,并没有明确的架构师职位。如果有,我们也希望这个角色应该是Martin Folwer认为的Architectus Oryzus,这些架构师与处理最困难部分的其他开发人员合作,积极地向项目贡献代码。显然,我们认为架构师也是程序员。这正是我们要在这个读书雷达中列出Architecture & Design象限的原因。

整体概览本象限列出的书籍,若有相关读书经验的人一定会注意到,这里列出的书籍并没有脱离面向对象设计体系的范畴。我们认为,在当今的企业级软件开发领域中,面向对象的思想体系仍然占据了主要的地位。对于程序员而言,建立面向对象的设计思维仍然至关重要。但是,我们从来都不曾忽略函数式语言对设计领域带来的冲击,以及它可能产生的巨大影响。在最新的ThoughtWorks Technology Radar的Languages & Frameworks象限中,ThoughtWorks对Scala,Clojure,F#等函数式语言青睐有加,同时也谈到了在Java中引入高阶函数等函数式特性的Functional Java发展趋势。我们也注意到在2012年的DDD eXchange会议上,函数式语言对设计范式的转变产生的影响。遗憾的是,除了少量介绍具体函数式语言的书籍,我们很少看到如面向对象设计一般专门讲解与深入探讨函数式软件设计的书籍。与其滥竽充数,不如抱残守缺。我们选择了在这期读书雷达图中,让函数式设计范式集体缺席。

架构内容包罗万象,在这个象限中我们关注的书籍主要与架构本质内容相关,包括架构风格、架构模式和重要的架构设计原则。虽然云计算、大数据以及REST服务等相关技术已经成为架构师的必备知识,也诞生了许多优秀的书籍;但我们还是希望让这个象限的内容变得更内聚一些,从而帮助读者能够从中挑选出合适的书籍,组成学习架构与设计技能的读书路线图。或许,在将来我们希望引入Tools、Frameworks以及Platforms等更多的象限来囊括这部分内容。

Struts 1.x一路走好

| Comments

因为Struts 1.x宣布退出了历史舞台,于是InfoQ组织了一次虚拟访谈。恰好在我现在的项目中,仍然能够看到Struts 1.x的身影,以我这浅薄的Web开发经验,也能有幸被丁雪丰邀请参加了这次虚拟访谈,和李锟、张龙就这一事件畅谈了各自的感受和想法。这篇虚拟访谈发表在了InfoQ上。这里发布的则是我自己就主持人提出的问题给出的回答。

1. Struts是最早的MVC框架之一,影响了很多人,你是否还记得最早接触到它时,给你留下的印象是什么?

张逸:从业15年来,我主要参与的开发工作除了早期的Windows Form应用开发外,主要还是集中在后端。用分层的角度来说,即工作在数据层、领域层以及服务层。虽然早已知道Struts的大名,甚至了解到所谓Struts+Spring+Hibernate几乎成为了Java企业开发的标配,却一直没有机会使用Struts。我当时工作的项目主要还是在微软的.NET平台上,经历了从ASP到ASP.NET,再到ASP.NET MVC的过程。在使用ASP时,我很质疑那种业务代码与表现代码混杂在一起的开发方式;而在最初拥抱ASP.NET时,我认为这种Code-Behind会是一种良好的职责分离。然而,ASP.NET在灵活性或扩展性方面带来的约束,使得它越来越不适合富客户端的开发了。于是,才有了借鉴Ruby On Rails思想的ASP.NET MVC出现。或许我的回答偏题了,但我想借ASP的这种发展来看待此次Struts 1.x退出历史舞台的事件,那就是任何产品都会步入晚年的衰落期,跟不上技术的发展,必然会被淘汰,没有什么好奇怪的。

从Go语言的设计学习设计决策

| Comments

阅读了Rob Pike撰写的《Go在谷歌:以软件工程为目的的语言设计》,颇多感触。这些感触并不在于语言层面,或者Go这门语言的语法以及底层实现;而是因为语言设计者们在设计Go这门语言时,做出设计决策的合理性以及基于的事实与根据。正如此文标题所言,显然,Go的创造者们从一开始就树立了准确的愿景与目标,并且清晰地确定了该语言的适用场景,即它需要解决“由多核处理器、系统的网络化、大规模计算机集群和Web编程模型带来的编程问题”,这是理解这门语言,进而明确其设计意图的最根本所在。

刚刚参加了公司八叉大神组织的轮子大赛,我们写了一个轻量级的IoC容器Melt。就这个容器或者说框架本身,和八叉讨论了框架设计的特点。他提到“framework开发和功能开发的一个最大分别就在于,你需要规定在这个framework里那些是支持的,哪些是不支持的。这条线要你自己来划,或者说你的framework要有态度。”这是我非常认同的观点。Framework要有态度,这意味着你在践行并且在引导一种最佳实践。或者我们可以理解为这是一种架构的约束。我们都知道,在软件设计中,如果没有任何约束,带来的问题反而会更大。约束是一种驱动力,例如我们需要可伸缩性的约束,就需要我们设计的服务不应该是有状态的。框架的态度大意如此。

回过头来看这篇文章介绍的Go设计理念,无时无刻不是在体现设计者施加在这门语言身上的态度。必须注意,这种态度或者说设计理念又绝对离不开这门语言的设计愿景。若是脱离这种具体场景来看Go语言,或许有众多不合理之处,但我们并不能依此妄加论断。正如话剧演员在舞台上的表演,总带着几分略带夸张的表情与语气,我们却不能指责这种表演不够生活化。当我们在思考一个设计决策是否合理时,是否参考了当时的场景做出判断呢?进一步讲,当我们自己在进行设计决策时,又是否充分地考虑了具体的场景呢?例如,Go语言之所以采用C语言风格的花括号,其考量并非简单地延续C语言风格那么简单,在前面提及的愿景的大前提下,设计者必须考虑如果使用Python或Haskell风格的空格缩进,对于大规模程序而言,可能会造成太多的问题。如文中所云:“我们的观点是,虽然空格缩进对于小规模的程序来说非常适用,但对大点的程序可不尽然,而且程序规模越大、代码库中的代码语言种类越多,空格缩进造成的问题就会越多。为了安全可靠,舍弃这点便利还是更好一点,因此Go采用了花括号表示的语句块。”

ThoughtWorks读书雷达-编码实践篇

| Comments

期望通过四分之一的读书雷达图就能将与编码实践有关的优秀书籍一网打尽,自然是不现实的打算。因此,我们希望就我们的侧重点来推荐书籍。对于编码实践而言,我们共同认为培养良好的编码习惯,编写整洁简单而又合理的代码,是一名好程序员的基本要求。因此,这里我们更强调与程序员基本编码技能相关的知识。我们并没有给出与算法直接有关的书籍,虽然我们认为算法知识同样属于编码实践的范畴,虽然我们认为诸如《计算机程序设计的艺术》、《编程珠玑》、《算法导论》之类的书籍同样很重要很优秀;然而,我们取舍再三,仍然将它们划出了读书雷达的范围。我们认为:算法知识更应该划定到大学教育的范畴,若工作需要,则又偏向于更为专精的领域,并不适合读书雷达这种普适性的推荐。相对于具体的算法,或许我们更看重程序员的逻辑思维以及抽象建模的能力。

在Coding Practice象限的Fundamental圈中,我们强烈推荐了Robert Martin的Clean Code《代码整洁之道》与The Clean Coder程序员的职业素养》,以及Martin Fowler的Refactoring《重构:改善既有代码的设计》。我不知道有多少人是阅读了Clean Code之后,才开始自己的整洁代码之旅;至少在我身边,这样的例子不胜枚举。把代码写成像散文那样美好,不仅仅是对美学的追求,更重要的是它能够极大地降低维护成本。在某种程度上讲,代码可以说是软件系统的质量基石。虽然重构的重要性被一直不断地提起,但我们发现真正掌握了重构手法的程序员,仍然屈指可数。通过对Refactoring一书的阅读,弄清楚什么是代码的坏味道,继而运用正确的重构手法,就能保证代码足够的整洁,甚至优雅。Robert Martin的另一本书Clean Coder与Clean Code一字之差,内容却大相径庭。它更多地是对程序员自身修养的关注。我们之所以强烈推荐它,并将其放入Fundamental圈中,是因为它介绍的知识,能够有效地帮助新入职场的程序员从一开始就能建立良好的编码习惯与意识。我们认为,这种好的习惯与意识,甚至比掌握某种开发技能显得更为重要。

遗留系统的技术栈迁移

| Comments

什么是遗留系统(Legacy System)?根据维基百科的定义,遗留系统是一种旧的方法、旧的技术、旧的计算机系统或应用程序[1]。这一定义事实上并没有很好地揭露遗留系统的本质。我认为,遗留系统首先是一个还在运行和使用,但已步入软件生命周期衰老期的软件系统。它符合所谓的“奶牛规则”:奶牛逐渐衰老,最终无奶可挤;然而与此同时,饲养成本却在上升。这意味着遗留系统会逐渐随着时间的推移,不断地增加维护成本。

维护一个软件系统,就需要了解该软件系统的知识。若知识缺失,就意味着这会给维护人员带来极大的障碍和困难。从这个角度讲,所谓“遗留系统”,就是缺少了一部分重要知识,使得维护人员“知其然而不知其所以然”的软件系统。

若要让遗留系统焕发青春,最彻底的做法自然是推倒重来,但这样付出的代价太高;而且,即使对系统重新设计和开发,仍然免不了会重蹈遗留系统的覆辙。或者,可以对遗留系统进行重构,在不修改系统功能的情况下改善系统设计。只是这种重构常常是对系统进行重大扩展或修改的前奏,如无绝对必要,并不推荐这种偿还“技术债务(Technical Debt)”的方式。重构应与开发同时进行,而不应将其作为债务推迟到最后,以至于支付高昂的利息。最后,还有一种方式,则是对遗留系统进行技术栈迁移。

ThoughtWorks(中国)程序员读书雷达

| Comments

软件业的特点是变化。若要提高软件开发的技能,就必须跟上技术发展的步伐。埋首醉心于项目开发与实战,固然能够锤炼自己的开发技巧,却难免受限于经验与学识。世界上并不存在速成的终南捷径,但阅读好的技术书籍,尤其是阅读大师们的经典著作,总能收到事半功倍之效。一位优秀的程序员,或许就是一名好的阅读者。好的阅读者,总是知道如何选择好的书籍。书海浩繁,良莠不齐。阅读技术好书,如与智者交谈,“与君一席话胜读十年书”;遭遇技术烂书,如被拐卖,“少小离家老大回,乡音无改鬓毛衰”。

ThoughtWorks作为一家学习型组织,颇为看重每一位员工的学习能力。事实上,大多数ThoughtWorker的骨子里,都溢满了读书的基因。与书相伴,与书为伍,既是一种乐趣,又是一种习惯。当习惯成为自然时,书籍就成为生活和工作不可或缺的一部分了。如果说人文历史哲学等书籍是一碗心灵鸡汤,技术书籍大抵算得上是一味营养素,读之可以直接带来养分;可若是不了解自己究竟缺了哪一种营养,乱吃乱补,结果就可能适得其反了。有鉴于此,我和同事刘龙军结合自身的阅读经验,为新入职ThoughtWorks的程序员制作了一份读书路线图。我们将范围明确为程序员,是因为作为程序员的我们,它是我们最了解的工作角色。我们筛选出了一些大家公认的经典书籍,再结合自己的阅读体会,并广泛征集了更多ThoughtWorker的意见,包括徐昊,熊节,郑晔等资深阅读者,得到了这样一份草稿。在中国公司内部推出时,这份读书路线图得到了多数人的认可和欢迎。继而,我们在成都办公室陆续针对此读书路线图展开了读书俱乐部的活动,算是做了一次全方位大面积的试水。

阅读Scala代码之二

| Comments

今天要阅读的代码来自《Scala By Example》一书的第一个例子。这两段代码通过实现一个快速排序算法体现了命令式与函数式之间的区别。这种直观的对比无疑很好地展现了函数式编程的优雅与简洁。让我们来看看这两段代码,首先是命令式的实现方式:

  def sort(xs: Array[Int]) {
    def swap(i: Int, j: Int) {
      val t = xs(i); xs(i) = xs(j); xs(j) = t
    }
    def sort1(l: Int, r: Int) {
      val pivot = xs((l + r) / 2)
      var i = l; var j = r
      while (i <= j) {
        while (xs(i) < pivot) i += 1
        while (xs(j) > pivot) j -= 1
        if (i <= j) {
          swap(i, j)
          i += 1
          j -= 1
        }
      }
    }
    if (l < j) sort1(l, j)
    if (j < r) sort1(i, r)
    sort1(0, xs.length - 1)
  }

下面是函数式的方式:

  def sort(xs: Array[Int]): Array[Int] = {
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length / 2)
      Array.concat(
        sort(xs filter (pivot >)),
             xs filter (pivot ==),
        sort(xs filter (pivot <)))
    }
  }

后者的简洁不言而喻,感觉实在太强烈了。它还展露出一种优雅的从容,因为没有嵌套的while循环,清扫了许多阅读障碍,没有繁文缛节,直指问题本质,显得游刃有余,挥洒自如。究其根由,在于这种函数式的编程方式,完全匹配快速排序的算法原则与过程,就像是那种斩钉截铁的证明,没有多余的啰嗦,结果如同“清水出芙蓉,天然来雕饰”。

不提这种感觉的美感,函数式编程带来的实实在在好处在于它的无副作用特质。这就好似你寻找的药方,不仅能够药到病除,服用后还没有不良反应,真可以说得上奢望了。阅读第二段代码,我们可以非常直观地看到没有任何操作修改了传入的xs数组。从外向内看,返回的数组是通过Array.concat将三段数组给串联了起来,返回了一个新的数组对象。表面看来,这段代码对xs做了filter操作,根据传入的Predicate对数组元素进行筛选。事实上,filter同样是函数,它并没有直接更改被操作的数组,而是返回了一个新的筛选后的数组对象。这意味着,即使我们传入一个val的数组对象,这个sort函数也是不会抱怨的。例如:

val target = Array(1, 2, 5, 3, 8, 4, 7)
val result = sort(target)

这两种方案皆使用了递归,时间复杂度皆为O(N log(N)),但就简洁性和易读性而言,却不可同日而语。而这种无副作用特性则体现了函数式的不变特质,从而可以极大地简化并发编程模型。当然,这种方法必然会造成空间的浪费;不过,有JVM提供的GC负责内存管理,我们也无需关心这些对象在何时需要被释放。只要系统对内存的要求没有特别的限制,这一问题几乎可以忽略不计。

好吧,让我们再转到Scala语言层面的特性上来。看第一段代码,除了个别关键字与语法不同之外,它几乎与Java代码没有太大的区别,最大的不同还在于Scala将函数(或者说方法)提升到了一等公民。第二段代码中,比较特殊的用法是调用Array的filter函数。该函数的签名为:

def filter(p: T => Boolean): Array[T]

调用时,这段代码传入的表达式比较奇怪。严格意义上,filter显然需要传入一个函数,这个函数要求一个输入参数,返回为Boolean型。如果采用匿名函数的方式,调用方式应该为:

xs.filter(x => pivot > x)

如果使用变量的placeholder,则可以表示为:

xs.filter(_ => pivot > _)

而这里使用的则是一种称为partially applied function的方式,它支持我们在不会引起歧义的情况下(主要是指只有一个参数的情形),直接省略该参数变量。只要明白这种语法,这样的代码仍然是可读的。

阅读Scala代码之一

| Comments

学习一门语言,固然需要了解这门语言的语法,但针对一些完全属于不同范式的语言,即使通过阅读书籍可以理解一些特殊的语法,若不能付诸实践,总有隔靴搔痒之感。其实,要能通过运用这门新语言开发一个项目,或能快速并深刻地了解甚至吃透这门语言。我正是这样尝试着运用Scala来开发我的一个开源框架。可是,在开发过程中,我总感觉自己像是被捆绑了一只手的程序员一般,开发过程磕磕碰碰,不够顺畅。仔细想来,还是因为缺乏对这门语言的足够了解,尤其是那些迥异于Java却又在Scala中是极为常见的惯用法,总不能做到在合适的场景信手拈来。

关键在于,自己阅读Scala的代码太少,编写Scala的代码更少。找到症结,那就尝试去解决。当然,我可以选择一些著名的Scala开源框架,例如Play Framework,Kestrel或者Kafka,对其进行深入阅读。可是,我发现这些框架对于目前的我而言,似乎显得困难了一点。那么,就从一些短小的代码段开始着手吧。