代码改变世界

或许你需要一些可操作性更强的实践

2011-01-28 22:55  横刀天笑  阅读(...)  评论(... 编辑 收藏

前几天,园子里评论比较高的一篇文章是《如何向妻子解释OOD》,这篇文章用交谈的方式,用生活中的示例来讲述面向对象编程的一些相关概念。一般这样的文章读起来都会朗朗上口,也非常好理解,因为都是生活中实际的例子。但是也有读者在下面的评论里说,他以前经常看一些用小猫小狗,汽车等用来讲解面向对象的文章,看的时候很有感觉,但是事后很快就遗忘了,更别说应用到自己的项目中。

实际上这不难理解,面向对象本身的目的就是提高抽象层次。更抽象的东西当然也就更难理解,也更难操作。所以大部分面向对象的书籍也都以一些具体的示例来讲解,这样才好将一些东西落实,不然容易给人一种泛泛而谈的感觉。但是,因为每个人的项目都各有不同(或许是相同的,但每个人自己都认为不同-_-),除非你拿着我的项目给我讲解,不然总是难以给我很深的触动。那么作为学习者的我们要倒地如何学习面向对象编程呢?我想,我们必须寻找一种可操作性非常强的方法来,让我们养成一种习惯,然后再慢慢地提升抽象层次。可操作性强就意味着有明确的准入准出原则,有明确的度量标准;行就是行,不行就是不行(但万事万物没有绝对的,我想说的只是期望能用一种可操作强的方式帮助我们来培养出面向对象的思维习惯,如果你认为你已经能够熟练的使用面向对象,那大可以一笑而过)。

使用更有意义的命名

其实,在大部分编程活动中,我们都是在给各种各样的元素来命名,如果你取了一个好名字,不仅能让元素的职责马上清晰起来,而且能使代码更好维护。在命名的时候,注意尽量使用声明方式的词语,不要用实现来命名:比如GetUser显然比GetUserFromDataBase要好。如果你存在一个GetUserFromDataBase方法,很有可能后面会出现一个GetUserFromFile方法。我想,你应该能明白我的意思了吧,GetUserFormDataBase和GetUserFromFile更应该是两个兄弟类中的对等方法,这时迈向面向对象的第一步。

命名还有另外一个层次的意义:给一段表达式命名。如果一段表达式很难理解,你甚至要看好几眼,甚至要写一行注释来说明,那么我们为什么不将这个表达式提取为一个方法呢,然后给这个方法一个非常好的命名呢?提取方法之后,不仅代码更好理解了,而且这段表达式的抽象层次也就相应地提高了。而且,如果一个方法内部,各个部分理解的难易程度居然不一致,那么很可能是该方法内部各部分之间抽象层次不一致,有的地方太过于细节,有的地方又只覆盖大的方面。在《Clean Code》里Uncle Bob告诉我们,在同一级别的元素中抽象的层次应该是一样的。比如下面这段代码:

   1: String defaultSignature = journalService.getDefaultSignature(journal);
   2: if(StringUtils.isBlank(defaultSignature) && journal.hasEditor()){
   3:    return defaultSignature + journal.getEditor().getName();
   4: }

在上面的代码中就存在抽象层次不一致的地方,journal.hasEditor()抽象层次比StringUtils.isBlank(defaultSignature)和下面的那个拼接字符串的层次都要高。那么我们可以将上面的代码改写为下面这个样子:

   1: if(hasDefaultSignature(journal) && journal.hasEditor()){
   2:    return getEditorSignature(journal);
   3: }
   4:  
   5: private boolean hasDefaultSignature(Journal journal){
   6:     String defaultSignature = journalService.getDefaultSignature(journal);
   7:     return StringUtils.isNotBlank(defaultSignature);
   8: }
   9:  
  10: private String getEditorSignature(Journal journal){
  11:    return journalService.getDefaultSignature(journal);
  12: }

方法数目变多了,但是我们现在甚至可以直接从方法的名字就可以读出代码的逻辑了。可能有的同学会发现这里有个重复的调用,这个会在后面提及。
命名还有个要注意的是,不要在名称中使用and/or等字眼,如果你的方法或者类名居然要使用这样的字眼,那么很明显,你违反了单一职责原则。

更小的类,更小的方法

如果说命名还是不好操作(什么样的名字算好,什么样的算不好?什么样子的是抽象层次不一致?)。那么现在的这条原则就好操作得多了。我们可以在团队里实行这样的原则:类不要超过150行,方法不要超过12行(额,为什么是150和12,为什么不是160和15?不好意思,我这是随手敲出来的,争论具体的数字是没有意义的,我想说的是使用一个可量化的标准,定下来并且获得团队内部所有成员的认可和遵守)。

每当你看到一个庞大的类或这方法时,你就尝试通过不断地提取新类(将干的事儿差不多的方法提到新的类中),提取新的方法(通过上面一条说的方式)将类和方法的规模不断地缩小。小的类和方法不仅非常容易读懂,而且重用的机会也就更大。小类和小方法有很多好处,你要尝试之后才知道,真的~,但是下面这一条却更为重要,也是走向面向对象设计的必由之路。

消除重复代码(逻辑)

随着项目地进行,我们的代码库不断地增长,必定会存在一些重复的地方。相同的代码,相同的逻辑充斥在各个地方。重复的代码大部分时候是因为Copy&Paste造成的,大部分人看看这两个地方实现相同,然后复制过来,稍做修改,就宣告完成。但这也是引入bug的最佳时机,如果一个地方错了,那么处处出错,而且你也丧失了一次进行面向对象的机会。

我们不能容忍重复的代码,如果遇到两块重复的代码,我们就要想办法提取方法复用之,提取”辅助“类复用之,提取基类继承之。但是重复的的代码也有可能不是那么显而易见,还有可能是部分重复,部分不重复。这个时候我们就要借助一些重构的手段,比如我们可以将以前一些提取出去的方法内联进来,将调用顺序修改修改。突然我们发现重复代码一致了(这部分内容可以参见Martin Fowler的《重构》)。

一行代码只出现一个点

其实这个原则有个更学术的名字:迪米特法则(The Law of Demeter)。意思就是只和身边的朋友交流,你不要通过点点点获取一个很远的对象的状态,然后再来做些计算。这样打探别人的隐私是不好的,你应该思考一下你为什么要这样做?是不是现在这段代码放错了地方,也就是你越权了。比如下面这段代码:

   1: rover.Position.X += 1;
   2: rover.Position.Y += 3;

还好,这里只有两个点,但是你有没有想过,我为什么要通过这种方式:获取rover的位置,然后给它的x坐标加1来移动rover呢?你为什么不采用一种更好的方式:

   1: rover.Move(1,3);

让rover自己移动好了,至于内部的实现我不想知道,我不想知道你用的是直角坐标系还是大地坐标系。那么,你现在有没有那么一点感觉,你的代码更面向对象了呢?我觉得我是有的。

你太着迷这个类了

你写了一段代码,你惊奇的发现这段代码居然没有使用自己所在类的任何属性,然后却不断地打探另外一个类的内部属性,然后做些操作。这个时候你是不是要考虑一下:这段代码不应该放在这儿吧。

就如本文第一段代码所示,这段代码不断地去打探Journal对象内部的东西。但是对自己类的东西却一点都不感兴趣,好吧,我觉得我们应该将这段代码移动到Journal里面去,变成一个getSignature方法。

不要getter/setter

你有没有发现过,你有很多类只是一堆的getter/setter,其余的啥东西都没有。所以这个时候我们的”代码生成器“有用武之地了(我当然不是反对使用代码生成器)。很多人都是想都不想,就将getter/setter全部生成出来,而.NET的自动实现属性更简化了这一操作,让对象”随意地“暴露自己的内部状态变得更流行。如果你暴露了许多内部状态,后面的代码就很容易打探到对象内部状态,也就更容易将本应该属于这个对象的职责的代码写到别的类中,如果你没有暴露任何内部状态,我要干一件事儿的时候,我不能问你了,我只能请求你帮我完成,然后你这个对象就丰富起来了。这就是传说的”吩咐,不要询问“的原则。关于随意地添加getter/setter在《OOD启思录》这本书里有更好的讨论(另外,这真是一本好书)。

 

后记

可操作性强的实践还有很多,这里只是举几个例子。后面我会给出一些参考书籍。当然,因为要可操作性强,有的时候可能会太绝对,我想什么东西都是权衡的结果。还是那句话,如果你觉得你对面向对象已经驾轻就熟,那么是没有什么必要记住这些东西了,但是如果你像我一样,头脑简单,难以理解那些抽象的论述,而且对小猫小狗的示例也不感兴趣,那么你大可一试,我想应该是没有什么坏处的。

参考书籍

下面这些书籍是我觉得很值得一读的面向对象的书籍或文章,很多内容通篇都没有讲面向对象相关的东西,但是却将那些原则融入到一个个实践当中。

《软件开发沉思录 Thoughtworks文集》中的49页,<对象健身操>,本文很多内容也来自该文。

《Clean Code》Bob大叔的又一新作,没有讲OO,字里行间都是写好代码的劝告。

《重构》 如果你实在不知道到底如何将一堆乱代码变成好代码,如何识别出代码的臭味,那么你应该看看这本书,然后将里面的每个方法都操作一遍,一定受益匪浅。

《OOD启思录》 没有大大的道理,只有淡淡的实践。本书已经绝版,淘宝上有打印版。