优秀设计背后的核心概念其实并不高深。好比内聚性、松耦合、零重复、封装、可测试性、可读性以及单一职责。这七条评判代码质量的原则已经被普遍接受了。然而真正困难的是如何把这些概念付诸实践。理解力封装就是隐藏“数据、实现细节、类型、设计或者构造”,这只是设计出良好封装的代码的第一步而已。所以,本文接下来是一系列实践和规则练习,它能够帮助你将良好的面向对象设计原则变得更加具体,从而在现实世界中应用那些原则。 java
规则 程序员
1.方法只使用以及缩进。 算法
2.拒绝使用else关键字。 编程
3.封装全部的原生类型和字符串。 设计模式
4.一行代码只有一个“.” 运算符。 app
5.不要使用缩写。 编程语言
6.保持实体对象简单清晰。 函数
7.任何类中的实例变量都不要超过两个。 工具
8.使用一流的集合。 测试
9.不适用任何Getter/Setter/Property。
规则1:方法只使用以及缩进
你是否曾经盯着一个体型巨大的老方法而感到无从下手过。庞大的方法一般缺乏内聚性。一个常见的原则是将方法的行数控制在5行以内,可是若是你的方法已是一个500行的大怪兽了,想要达到这一原则的要求是很是痛苦的。其实,你不妨尝试让每一个方法只作一件事——每一个方法只包含一个控制结构或者一个代码块。若是你在一个方法中嵌套了多层控制结构,那么你就要处理多个层次上的抽象,这意味着同事作多件事。
若是每一个方法都只关注一件事,而它们所在的类也只作一件事,那么你的代码就开始变化了。因为应用程序中的每一个单元都变得更小了,代码的可重用性开始指数增加。一个100行的,肩负五种不一样职责的方法很难被重用。若是一个很短的方法在设置了上下文后,可以管理一个对象的状态,那么它能够应用在不少不一样的上下文中。
利用IDE提供的“抽取方法”功能,不断地抽取方法中的行为,直到它只有一级缩进为止。请看下面的实例。
class Board{ ... String board(){ StringBuffer buf = new StringBuffer(); for(int i=0; i < 10; i++){ for(int j=0; j < 10; j++){ buf.append(data[i][j]); buf.append("\n"); } } return buf.toString(); } }
class Board { ... String board(){ StringBuffer buf = new StringBuffer(); collectRows(buf); Return buf.toString(); } void collectRows(StringBuffer buf){ for(int i=0; i<10; i++){ collectRow(buf,i); } } void collectRow(StringBuffer buf, int row){ for(int i=0; i<10; i++){ buf.append(data[row][i]); } buf.append("\n"); } }注意这项重构还能带来另外一种效果:每一个单独的方法都变得更简单了,同时其实现也与其名称更加匹配。在这样短小的代码段中查找bug一般会更加容易。
第一条规则在此接近尾声了。我还要强调,你越多实践这条规则,就会越多的尝到它带来的甜头。当你第一次尝试解决前面展现的那一类问题时,可能不是很是的熟练,也未必能得到不少收获。可是,应用这些规则的技能是一种艺术,它能将程序提高到一个新的高度。
规则2:拒绝else关键字
每一个程序员都熟知if/else结构。几乎每种语言都支持if/else。简单的条件判断对任何人来讲都不难理解。不过大多数程序员也见识过使人眩晕的层层嵌套的条件判断,或者连绵数页的case语句。更糟糕的是,在现有的判断条件上加一个新的分支一般是很是容易的,而将它重构为一个更好的方式的想法却罕有人去说起。条件判断结构一般仍是重复代码的来源。例如,状态标识常常会带来这样的问题。
public static void endMe(){ if(status == DONE){ doSomething(); }else{ <other code> } }你有不少种方式重写这段代码,去掉else关键字。例以下面的代码。
public static void endMe(){ if(status == DONE){ doSomething(); return; } <other code> } public static Node head(){ if(isAdvancing()){ return first; }else{ return last; } } public static Node head(){ return isAdvancing()?first:last; }在上面的例子中,第二段代码因为使用了三元运算符,因此代码长度从四行压缩到了一行。须要当心的是,若是过分使用“提早返回”,代码的清晰度很快会下降《设计模式中》一书中关于策略模式的部分里有一个实例,演示了如何使用多态避免根据状态进行分支选择的代码。若是这种根据状态进行分支选择的代码大量地重复,就应该考虑使用策略模式了。
面向对象编程语言给咱们提供了一种更为强大的工具——多态。它可以处理更为复杂的条件判断。对于简单的条件判断,咱们可使用“卫语句”和“提早返回”替换它。而基于多态的设计则更容易阅读与维护,从而能够更清晰的表达代码的内在乎图。可是,程序员要作出这样的转变并非一路顺风的.尤为是你的代码中可能早已充斥了else。因此,做为这个练习的一部分,你是不可使用else的。在某些场景下可使用Null Object模式,它会对你有所帮助。另外还有不少工具和技术均可以帮助你甩掉else。试一试,看你能提出多少种方案来?
规则3:封装全部的原生类型和字符串
整数自身只表明一个数量,没有任何含义。当方法的参数是整数时,咱们就必须在方法中描述清楚参数的意思。若是此方法使用“Hour”做为参数,就可以让程序员更容易地理解它的含义了。像这样的小对象能够提升程序的可维护性,由于你不可能给一个参数为“Hour”的方法传一个“Year”。若是使用原生变量,编译器不能帮助你编写语义正确的程序。若是使用对象,哪怕是很小的对象,它都能给编译器和其余程序员提供更多的信息——这个值是什么,为何使用它。
像Hour或Money这样的小对象还提供了放置一类行为的场所,这些行为放在其余的类中都不合适。在你了解了关于getter和setter的规则时,这一点会很是明显,有些值只能被这些小对象来访问。
规则4:一行代码中只有一个“.”运算符
有时候咱们很难判断出一个行为的职责应该由哪一个对象来承担。若是你看一看那些包含了多个“.”的代码,就会从中发现不少没有被正确放置的职责。若是代码中每一行都有多个“.”,那么这个行为就发生在错误的位置了。也许你的对象须要同时与另外两个对象打交道。在这种状况下,你的对象只是一个中间人;它知道太多关于其余对象的事情了。这时能够考虑把该行为移到其它对象之中。
若是这些“.”都是彼此联系的,你的对象就已经深深的陷入到另外一个对象之中了。这些过量的“.”说明你破坏了封装性。尝试着让对象为你作一些事情,而不要窥视对象内部的细节。封装的主要含义就是,不要让类的边界跨入到它不该该知道的类型中。
迪米特法则(The Law of Demeter,“只和身边的朋友交流”)是一个很好的起点。还能够这样思考它:你能够玩本身的玩具,能够玩你制造的玩具,还有别人送给你的玩具。可是永远不要碰别人的玩具。
class Board { ... class Piece { ... String representation; } class Location { ... Piece current; } String boardRepresentation(){ StringBuffer buf = new StringBuffer(); for(Location l: squares()){ buf.append(l.current.representation.substring(0,1)); } return buf.toString(); } } class Board { ... class Piece { ... private String representation; String character(){ return representaion.substring(0,1); } void addTo(StringBuffer buf){ buf.append(character()); } } class Location { ... private Piece current; void addTo(StringBuffer buf){ current.addTo(buf); } } String boardRepresentation(){ StringBuffer buf = new StringBuffer(); for(Location l: squares()){ l.addTo(buf); } return buf.toString(); } }
注意在这个例子中,算法的实现细节被过分的扩散开了。程序员很难看一眼就理解它。可是在为Piece转化成character的行为建立一个具备名称的方法后,这个方法的名称和做用就至关一致了,并且被重用的机会也会很是高——使人费解的representation.substring(0,1)调用能够所有被这个具备名称的方法所代替,程序的可读性又迈进了一大步。在这片新天地里,方法名取代了注释,因此,值得花些时间为方法取一个有意义的名字。理解并写出这种结构的程序并不困难,你只需使用一些稍微不一样的手段而已。
规则5:不要使用缩写
咱们总会不自觉地在类名、方法名或者变量名中使用缩写。请抵制住这个诱惑。缩写会使人迷惑,也容易隐藏一些更严重的问题。
想想你为何要使用缩写。由于你厌倦了一遍又一遍的敲打相同的单词?若是是这种状况,也许你的方法调用的过于频繁,你是否是应该停下来消除一些重复了?由于方法的名字太长?这可能意味着有些职责没有放在正确的位置或者是有缺失的类。
尽可能保持类名和方法命中只包含一到两个单词,避免在名字中重复上下文的信息。好比某个类是Order,那么方法名就没必要叫作shipOrder()了,把它简化为ship(),客户端就会调用order.ship()——这可以简单明了的说明代码的意图。
在这个练习中,全部实体对象的名称都只能包含一到两个单词,不能使用缩写。
规则6:保持实体对象简单清晰
这意味着每一个类的长度都不能超过50行,每一个包所包含的文件不超过10个。
代码超过50行的类所作的事情一般都不止一件,这会致使它们难以被理解和重用。小于50行代码的类还有一个妙处:它能够在一个屏幕内显示,不须要滚屏,这样程序员能够很容易、很快熟悉这个类。
建立这样小的类会遇到哪些挑战呢?一般会有不少成组的行为,它们逻辑上是应该在一块儿的。这时就须要使用包机制来平衡。随着类变得愈来愈小,职责愈来愈少,加之包的大小也受到限制,你会逐渐注意到,包中的类愈来愈集中,它们可以协做完成一个相同的目标。包和类同样,也应该是内聚的,有一个明确的意图。保证这些包足够小,就能让它们有一个真正的标识。
规则7:任何类中的实例变量都不要超过两个
大多数的类应该只负责处理单一的状态变量,有些时候也能够拥有两个状态变量。每当为类添加一个实例变量,就会当即下降类的内聚性。通常而言,编程时若是遵照这些规则,你会发现只有两种类,一种类负责维护一个实例变量的状态;另外一种类负责协调两个独立的变量。不要让这两种职责同时出如今一个类中。
敏锐的读者可能已经注意到了,规则3和规则7实际上是相同问题的不一样表述而已。在一般状况下,对于一个包含不少实例变量的类来讲,很难拥有一个内聚的、单一的职责描述。
咱们来仔细分析下面的示例。
class Name { String first; String middle; String last; }这个类能够被拆分为两个类
class Name { Surname family; GivenName given; } class Surname { String family; } class GivenName { List<String> names; }
注意思考这里是如何分离概念的,其中姓氏(family name)是一个关注点(不少法律实体约束中须要用到),它能够和其余与其有本质区别的名字分开。GivenName对象包含了一个名字的列表。在新的模型中,名称容许包含first,middle和其余名字。一般,对实例变量解耦之后,会加深理解各个相关的实例变量之间的共性。有时,几个相关的实例变量在一流的集合中会相互关联。
将一个对象从拥有大量属性的状态,解构成为分层次的、相互关联的多个对象,会直接产生一个更实用的对象模型。在想到这条规则以前,我曾经浪费过不少时间去追踪那些大型对象的数据流。虽然咱们能够理清一个复杂的对象模型,可是理解各组相关的行为并看到结果是一个很是痛苦的过程。相比而言,不断应用这条规则,能够快速将一个复杂的大对象分解成为大量简单的小对象。行为也天然而然地随着各个实例变量流入到了适当的地方——不然编译器和封装法则都不高兴的。当你真正开始作的时候,能够沿着两个方向进行:其一,能够将对象的实例变量按照相关性分离在两个部分中;另外也能够建立一个新的对象来封装两个已有的实例变量。
规则8:使用一流的集合
应用这条规则的方法很是简单:任何包含集合的类都不能再包含其余的成员变量。每一个集合都被封装在本身的类中,这样,与集合相关的行为就有了本身的家。你可能会发现做用于这些集合的过滤器将成为这些新类型中的一部分。或是根据它们自身的状况包装为函数对象。另外这些新的类型还能够处理其余任务,好比将两个集合中的元素拼装到一块儿,或者对集合中的元素逐一施加某种规则等。很明显,这条规则是对前面关于实例变量规则的扩展,不过它自身也有很是重要的意义。集合实际上是一种应用普遍的原生类型。它具备不少行为,可是对于代码的读者和维护者来讲,与集合相关的代码一般都缺乏对语义意图的解释。
规则9:不适用任何Getter/Setter/Property
上一条规则的最后一句话几乎能够直接通向这条规则。若是你的对象已经封装了相应的实例变量,可是设计仍然很糟糕的话,那就应该仔细的考察一下其余对封装更直接的破坏了。若是能够从对象以外随便询问实例变量的值,那么行为与数据就不可能封装到一处。在严格封装边界背后,真正的动机是迫使程序员在完成编码以后,必定要为这段代码的行为找到一个合适的位置,确保它在对象模型中的惟一性。这样作会有不少好处,好比合一很大程度的减小重复性的错误;另外,在实现新特性的时候,也有一个更合适的位置去引入变化。
这条规则一般被描述为“讲述而不要询问”(“Tell, don't ask”)。
-----from ThoughtWorks 文集