《卓有成效程序员》第十章

10章.古代哲人


    在一本关于程序员生产率的书籍中看到讲述古代哲人的章节或许会让你感到突兀,但这是真的。结果表明,由古老(或者不是那么古老)的哲学家们发现的一些哲学思想对构建高质量软件有直接的影响。废话少说,让我们快去看看这几个哲学家是怎么评说代码的吧。

亚里斯多德的“事物的本质和附属性质”理论

   亚里斯多德建立了很多我们今天熟知的科学分支,事实上,多数科学研究都起源于他。他把自己对自然世界的所有思考进行分类,编目,并给出定义。同时,他也是逻辑和形式思维的奠基者。

亚里斯多德定义的一个逻辑原理是:事物本质性质和附属性质之间的区别。比方说,有五个单身汉,他们都有棕色的眼睛。未婚是他们的本质性质,而棕色眼睛是一个附属性质。因为你不能由此推断说:所有的单身汉都有棕色的眼睛,因为眼睛的颜色确实只是一个巧合。

好吧,但这跟软件何干?稍微延伸一下这个概念,便会让我们想到事物的本质复杂性和附属复杂性。本质复杂性是指要被解决之问题的核心,由软件中的难点问题组成。大多数软件的问题都包含一些复杂性。附属复杂性是指跟解决方案没有必要直接关联的那些东西,但无论如何我们仍然要解决它们。

举个例子。譬如说有一个问题,它的本质复杂性在于对客户数据的跟踪:从网页获取数据,并把它们保存到数据库。这是一个很简单很直观的问题,但是要让它在你的组织内工作,你必须使用一个由低劣驱动所支持的“古老”数据库。当然,你还要担心数据库的访问权限问题。如果在其他某处主机上的一些数据库包含相似的数据,还必须要用数据交叉检查以保证一致性。现在,就是要想办法连接到主机,然后以一种你可使用的格式把数据提取出来。这时,你却发现你无法直接连接到数据库,因为在你使用的工具中竟然没有连接器。没办法,你只能让其他人为你提取数据,并把它们存放到某个数据仓库,然后你可以从那里获取。这听起来像不像你正在干的工作?本质复杂性往往可以一言以蔽之,而附属复杂性却常常没完没了。

没人希望在附属复杂性上花费比在本质复杂性上还多的时间,但随着它的累积,很多组织最终在其上要花费比本质复杂性更多的时间。SOA(面向服务的架构)之所以在当前这么流行,就是因为很多公司试图从日积月累的大量附属复杂性中抽身而出。SOA是一个把迥然相异的两个应用程序绑定在一起,使之能够相互通信的架构风格。很少人会认为这是驱使你使用SOA的原因。然而,这的确就是在你有很多需要分享信息却不可通信的程序时想做的事情。但在我看来,这纯粹是平添附属复杂性的行为。在厂商嘴里,SOA架构风格就等于企业服务总线(ESB,其主要卖点是:中间件问题的解决方案是使之更加中间化。难道增加复杂性会降低复杂性么?几乎不可能。所以,要对厂商驱动的解决方案慎之又慎。他们的首要目的是销售他们的产品,其次才(或许)是让你的生活更加美好。

识别问题是摆脱附属复杂性的第一步。思考一下你所使用的过程、策略、以及正在处理的技术难题。认识清楚怎样的改进可能从根本上让你抛弃一些对整个问题贡献不多却平添麻烦的东西。就比如刚才那个问题,你可能认为你需要的是一个数据仓库,但其实它所能带来的好处远比其增加的复杂性要少。你不可能把软件中的附属复杂性统统干掉,但是你可以致力于不断减少它们。

提示

致力本质复杂性,干掉附属复杂性。

奥卡姆剃刀原理

奥卡姆*的威廉爵士是一个厌恶华美装饰以及复杂解释的修士。他对哲学和科学的贡献是奥卡姆剃刀原理:如果对于一个现象有好几种解释,那么最简单的解释往往是最正确的。显然,这跟我们讨论的事物本质和附属性质理论紧密关联。这个原理对于软件的影响度也是出乎我们意料的。

作为软件工业中的一员,过去十年我们一直在进行着某项实验。这个实验始于上世纪90年代中期,主要是由于开发人员发现其开发进度远远跟不上软件需求的增长而引发的(其实在那时这已经不是一个新问题,这个问题自商业软件的想法出现之后就一直存在)。实验的目的是:创造一些工具和环境来提高那些普通开发人员的生产率,即使一些人比如Fred Brooks(去看他的《人月神话》)已经告诉我们软件开发中的一些混乱事实。此实验试图验证:我们是否可以创造一种能限制程序员破坏力的语言而使人摆脱麻烦;我们是否可以无需支付荒唐的大量金钱给那些令人生厌的软件技工(即使在那时候你可能还为找不到足够的软件技工而发愁),而同样生产出软件呢?这些思考让我们创造出了如dBase, PowerBuilder, ClipperAccess这样的工具,并促成了工具和语言相结合的4GL(第四代语言)的崛起,比如FoxProAccess

但问题是,即使有这样的工具和环境你也不能完成所有的工作。我同事Terry DietzlerAccess创建了一个叫做“80-10-10”的准则(而我喜欢把它称之为Dietzler定律)。这个定律说的是:80%的客户需求可以很快完成;下一个10%需要花很大的努力才能完成;而最后的10%却几乎是不可能完成的,因为你不能把所有的工具和框架都“招致麾下”。而用户却希望能满足一切需求,所以作为通用目的语言的4GLVisual BASICJavaDelphi以及C#)应运而生。JavaC#的出现主要是由于C++的复杂性和易错性,语言开发者们为了让一般程序员摆脱这些麻烦而在其内建了一些相当严格的限制。在此之后“80-10-10准则”才发生了改变,无法完成的工作已经微乎其微。这些语言都是通用目的语言,只要付出足够的努力,大多数工作都可以完成。但Java虽然比较易用却常常需要大量编码,所以框架出现了,Aspects出现了,大量其它框架蜂拥而至。

下面有一个例子。这段Java代码是从一个广泛使用的开源框架中提取出来的,试着找出它的用途吧(关于它的名字我只会提示你一点点):

public static boolean xxXxxxx(String str) {

      int strLen;

      if (str == null || (strLen = str.length()) == 0) {

          return true;

      }

      for (int i = 0; i < strLen; i++) {

          if ((Character.isWhitespace(str.charAt(i)) == false)) {

              return false;

          }

      }

      return true;

  }

花了多少时间?这实际上是一个从Jakarta Commons框架(它提供了一些或许本该内置于Java的帮助类和方法)中提取出来的isBlank方法。一个字符串是否为“空白”由两个条件决定:这个字符串是空字符串,或者它只由空格组成。这段代码的计算公式非常复杂,因为要考虑参数是null的情况,而且还要迭代所有的字符。当然,你还要把字符包装成Character类型以确定它是否空白字符(空格、制表符、换行符等)。总之,太麻烦了!

下面是用Ruby实现的版本:

class String

  def blank?

    empty? || strip.empty?

  end

end

这个定义跟之前的那个非常相近。在Ruby里,你可以把String类打开并且加入一些新方法。这个blank?方法(Ruby里返回布尔值的方法通常用问号结尾)会检查字符串是否为空,或者在去除所有空格之后是否为空。在Ruby里方法的最后一行就是返回值,所以你可以忽略可选的return关键字。

这段代码在一些预想不到的情况下同样工作。看看这段代码的单元测试吧:

class BlankTest < Test::Unit::TestCase

  def test_blank

    assert "".blank?

    assert " ".blank?

    assert nil.to_s.blank?

    assert ! "x".blank?

  end

end

1. Ruby里,nilNilClass类的一个对象,就是说它也有to_s方法(等价于JavaC#中的toString方法)。

其实我讲这些的主要意旨是:一些在“企业级开发”中非常流行的主流静态类型语言有很多附属复杂性。Java中的基本数据类型就是一个语言附属复杂性的绝佳例子。当Java还是新语言的时候它们确实非常有用,但如今它们只是让代码更加难以看懂而已。自动装箱*能起到一些帮助,但是它会引起其它一些异常问题。看看下面这段代码吧,肯定会让你挠头:

public void test_Compiler_is_sane_with_lists() {

    ArrayList list = new ArrayList();

    list.add("one");

    list.add("two");

    list.add("three");

    list.remove(0);

    assertEquals(2, list.size());

}

这个测试通过了。再看看下面这个版本,它们之间的区别只有一个单词(ArrayList被换成了Collection):

public void test_Compiler_is_broken_with_collections() {

    Collection list = new ArrayList();

    list.add("one");

    list.add("two");

    list.add("three");

    list.remove(0);

    assertEquals(2, list.size());

}

这个测试失败了,抱怨说list的大小还是3。说明了什么问题?它很好地解释了当使用泛型和自动装箱来改装一些复杂的库(比如集合)时会发生什么事情。测试失败的原因在于:Collection接口虽然也有remove方法,但是它删除的是与其参数内容匹配的项,而不是索引指定的项。在这个例子中,Java把整数0动装箱成一个Integer类的对象,然后在列表中寻找一个内容是0的项,当然最后没有找到,所以也就不删除任何东西。

现代语言非但没有让程序员摆脱麻烦,而且其附属复杂性还迫使他们艰难地寻找复杂的解决方法。这个趋势影响了构建复杂软件的生产率。我们真正想要的,是4GL作为强大的通用目的语言所具有的通用性和灵活性所带来的高生产率。让我们看看用DSL(领域特定语言)构建的框架,目前一个很好的范例是Ruby on Rails。当编写Rails程序时,你不用写太多“纯”Ruby代码(即使写,大部分也只在处理商业逻辑的模型层)。你主要是在RailsDSL部分编写代码,这意味着编码工作都投入在最有价值的部分,当然也带来了更大的产出。

  validates_presence_of :name, :sales_description, :logo_image_url

  validates_numericality_of :account_balance

  validates_uniqueness_of :name

  validates_format_of :logo_image_url,

                      :with => %r{\.(gif|jpg|png)}i,

                      :message => "must be a URL for a GIF, JPG, or PNG image"

只要小小的一段代码,却实现了大量的功能。DSL构建的框架提供了4GL级别的生产率,但它们有一个关键的差异。用4GL(目前一些主流的静态类型语言)做一些非常强大的东西(比如元编程)是极其困难或者说不可能的,而在一个基于一种超级强大语言的DSL里,你不仅可以用少量的代码完成大量的功能,而且可以跳到底层语言去实现任何你想做的东西。

“强大的语言加上特定领域元层次”提供了目前最好的解决方案。生产率来自DSL跟问题域的紧密相连;能力来自于表层之下的强大语言。基于强大语言并且易于表达的DSL将会成为一个新的标准。框架将会用DSL来编写,而不再是语法拘束且无必要规范过多的静态类型语言。请注意这并不代表只能使用动态语言(比如Ruby),只要有合适的语法,静态类型推断语言也非常有可能从这种设计风格中获益。比方说Jaskell[43],特别是基于它的DSLNeptune[44]Neptune拥有和Ant一样的基本功能,但它是基于Jaskell的领域特定语言。Neptune展示了在一个熟悉的问题域之内,你可以用Jaskell写出多么易读以及简洁的代码。

提示

Dietzler定律:即使是通用目的编程语言也逃不出“80-10-10准则”的魔咒。

笛米特法则

笛米特法则是上个世纪80年代晚期在美国西北大学发展起来的。可以用一句话总结这个法则:只跟最亲密的朋友讲话。它的主要思想是:任何对象都不需要知道与之交互的那些对象的任何内部细节。这个法则的名字来自于古罗马掌管农业(也就是掌管食物分配)的女神笛米特。虽然严格地讲她不是一个古代哲学家,但她的名字听起来确实很有哲学家味道。

更正式地说,笛米特法则讲的是任何一个对象或者方法,它应该只能调用下列对象:

  • 该对象本身
  • 作为参数传进来的对象
  • 在方法内创建的对象

在大多数现代语言中,你可以把它解释得更形象更简短:在调用方法时永远不要使用一个以上的“点”。下面就是一个例子。

有一个Person,它有两个属性:nameJobJob类同样有两个属性:titlesalary。根据笛米特法则,在Person里面通过调用Job得到其position属性是不被允许的,就如下面一样:

 Job job = new Job("Safety Engineer", 50000.00);

  Person homer = new Person("Homer", job);

 

  homer.getJob().setPosition("Janitor");

那么,要遵循笛米特法则,你可以在Person类里面创建一个方法来改变job,然后让Job类去完成余下的工作,看下面:

public PersonDemo() {

    Job job = new Job("Safety Engineer", 50000.00);

    Person homer = new Person("Homer", job);

    homer.changeJobPositionTo("Janitor");

}

 

public void changeJobPositionTo(String newPosition) {

    job.changePositionTo(newPosition);

}

这样的改变带来了什么好处?首先请注意,我们不再调用Job类的setPosition方法,而是使用了一个更形象的名字:changePositionTo。这强调了一个事实:除了Job类本身,没有任何其他东西知道Job类内部的position是如何实现的。虽然它现在看起来像是一个字符串,但在内部实现上可能使用的是一个枚举。这么做的主要目的是信息隐藏:你不想让依赖类知道被依赖类内部工作的实现细节。笛米特法则通过迫使你编写隐藏细节的方法来达到这个目的。

当严格遵守笛米特法则时,你会倾向于为你的类做很多小的包装或者为其编写大量的代理方法,以防止在调用方法时使用多个“点”。那段额外的代码让两个类之间的耦合更加松散,这样能保证一个类的改变不会对另一个类产生影响。如果你想看更详尽的例子,请阅读David Bock[45]的文章“The Paperboy, The Wallet, and The Law Of Demeter”(也是软件学说的一部分)。

古老的软件学说

软件开发者们对“古老的”软件学说基本上一无所知。因为软件技术日新月异,开发者们为了保持与时代的同步已经需要付出很大的努力,而且那些(相对)古老的技术能帮助我们解决当前的问题么?

当然,看一本Smalltalk的语法书并不能帮助你提高Java或者C#水平,但Smalltalk书籍不仅仅讲述语法,比如它们可以让那些第一次使用全新技术(面向对象语言)的开发者们学到很多难得的知识。

提示

关注那些“古老的”软件技术学说。

古代哲学家们所创立的一些在现在看来显而易见的思想,在当时却需要非凡的才智和莫大的勇气。有时候,他们因为其思想违背了当时已建立的教条还要遭受极大的迫害。其中一个伟大的“历史背叛者”就是伽利略,他不相信任何人告诉他的任何事情,凡事他都要自己进行尝试。在他之前的时代,人们已经接受了“重物比轻物降落得快”的说法。这是亚里士多德学派的思想,他们认为逻辑思考比实验更有价值。伽利略却不买账,他登上比萨斜塔然后向下扔下石块,他还用大炮射击石头。最后他终于发现这个有悖直觉的事实:所有的物体都以相同的速度降落(如果不计空气阻力)。

伽利略验证了:有些看起来不符合直觉的事情其实是正确的。同时,这也是他给后人上的非常有价值的一课。很多“实打实”的软件开发知识并不符合直觉。比如,“软件可以预先设计好并按部就班地去实现”的想法看起来是符合逻辑的,但在现实世界的持续变化面前,它并不成立。幸运的是,大量反直觉的软件学说已经编目在反模式目录(http://c2.com/cgi/wiki?AntiPatternsCatalog)中。这是软件的古老学说。当你的老板要求你使用一个低质量的代码库时,不需要在崩溃中咬牙切齿,告诉他:你正落入“站在侏儒的肩膀上”的陷阱中,然后他就会明白不仅仅只有你才觉得那是个坏主意。

理解已存在的软件学说,能给你提供很好的资源。比如当你被告诉甚至被一些管理者强迫去做一件你内心知道是错的事情时。理解过去发生的战争能为你当前的战争供给弹药。花一些时间去读读那些数十年之前就已面世但仍被广泛阅读的软件书籍吧,比如《人月神话》,HuntThomas的《程序员修炼之道》(Addison-Wesley)以及Beck的《Smalltalk Best Practice Patterns》(Prentice Hall)。这远远不是一个详尽的列表,但这几本书都提供了无价的知识。

 [43] http://jaskell.codehaus.org/ 下载

[44] http://jaskell.codehaus.org/Neptune 下载

[45] http://www.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/paper-boy/demeter.pdf 下载

 



* 译者注:奥卡姆(Ockham)在英格兰的萨里郡,是威廉出生的地方。

* 译者注:自动装箱(Autoboxing)是指将基本的数据类型自动地转换为封装类型。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/16502878/viewspace-573134/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/16502878/viewspace-573134/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值