面向对象程序设计有三个特征:封装、继承、多态。
这三个特征即是语法也是手段,23种设计模板对这三种手段的灵活应用。
封装
基于框架开发的时代,开发只是往框架里“填充”业务代码。似乎不太需要考虑太多东西,这时我觉得“封装”就很重要了,“封装”就是开发层面的“微设计”。
1 认清关系
A调用B什么关系?调用和被调用的关系?No,No,No!
A为什么调用B?
因为A不调用B,就无法完成自己的工作,B能帮助完成工作。也就是说A依赖于B。
这个认识直接影响到代码的组织。
1.1 向下原则
依赖者之间的位置或者层次是什么样的?
答案是上下的,不是平行的。也就是B在A下层,为A提供服务,这个可以参照ISO七层模型来理解,也可以参考我们的框架,web层->service层->dao层。《代码整洁之道》里称之为向下原则。
案例:
假设有个方法有300行代码,里面有些东西可以封装子F来,你会怎么做?
我相信大部分人做的都还可以。
但有这么个做法,把方法拆成了三个方法,F1,F2,F3,F1调用F2,F2调用F3,每个方法体里面基本上是之前方法的一段代码,比如F1是1100行代码,F2是101200行,F3是201到300行,三个方法需要的参数几乎都是一样的,通过形参依次传递下去,而且返回值都是void类型,给方法的名字和注释也很尴尬,意思都很接近。
这个做法如何?从形式上不行,每个方法都是原来方法身体的一部分。
从逻辑上他们是平行的,可以认为它还是一个方法,他们之间不是依赖关系。这样封装代码仅仅是把代码挪了个地方,还不如不拆分,阅读者得自己脑补,把代码合并起来才能知道到底想干什么。
把一个方法比喻成一个人,那么如果这个方法需要重构,抽离出来的方法仍然必须是个人,是个五脏六腑俱全的mini小人,而不是这个人的手或者脚。
1.2 封装不是简简单单的挪动代码
F1调用F2,F2调用F3,或者F1调用F2、F3都可能是合理的.
关键看几个F封装的符不符合我们常见的原则,后续我们将均使用上面的例子,因为它很巧的违反了很多原则.
后面是具体原则
2 单一职责原则
单一职责,强调的是职责的分离,一个方法只干一件事情。
只为一个原因做修改。
很多代码之所以需要重构,因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
从微观上讲单一职责的方法一目了然,职责明确,利于维护。
从宏观上讲是设计的要求。单一职责原则可以看作是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,职责过多,可能引起它变化的原因就越多,从而极大的损伤其内聚性和耦合度。
这个原则非常好理解,最常见的违反这个原则的例子就是写“大而全”的方法。
(一个方法干了太多事情)一个方法搞定各种逻辑,各种flag,各种分支判断,到最后代码看不出主逻辑是什么了。
如果单一职责原则做的好,代码都不会太长,随便翻开Apache的开源软件源码,大部分的方法都没超过100行,当然有些核心类、管理类等,长代码还是有的。
除了大而全的方法,想想上面的F1~F3的例子,有没有违反单一原则?
与大多数人职责过多相反,它是因没有职责而违反。
这里有个“诡辩”,只要不是空方法,只要方法里有代码、有逻辑就有职责,这一点我不太同意。这是个职责范围的认定问题,虽然职责的认定是仁者见仁智者见智的,但是我觉得有些基本的东西是确切的,比如职责必须很明确、完整。 上述F1~F3,每个方法都没有完整明确的职责,看代码的感受就是不知道这个方法想干什么,感觉每个方法的存在意义都不大,逻辑合并起开才勉强知道想干什么,所以我认为职责模糊的方法是违反单一原则的。
还有就是返回值,有返回值并且设置void的,也属于职责不明确。除非真的没有返回值可以用void,否都应该返回值。隐式返回容易让调用者遗忘哪些变量被改变了,进而引起编程bug。
3 最少知道原则
一个对象应该对其他对象保持最少的了解。
通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
函数层次上也要遵循此原则,方法的开口尽可能的小,入参之间不要有依赖,一个极端的例子:一个方法有3个入参,但是第1个和第2个能计算出第3个,就是违反最少知道原则,第三个参数应该在方法体里计算出来。
入参过多很可能会违反最少知道原则,上面的F1,F2,F3的例子,形参个数多达10个。有些很复杂的场景,传递的参数如果真的必须很多的话,考虑用有意义的实体封装,这个封装其实是对入参“归类”,虽然需要的参数还是需要一一“拿到”然后放进实体里,但是归成一类逻辑上可以看成“一个”。很多公司的规范对形参个数都有要求。
4、考虑线程安全问题
线程安全问题平时很难测试到,需要特别注意,时时注意。
静态变量、单例的成员变量都是可能被多个线程访问的、资源的非原子操作(例如数据库一个值读取出来后,再update+1)等等,设计类时把考虑线程是否安全当成一个习惯。
5、好的代码是什么样的
好多人常说优雅,优雅是代码的最高境界。我们不说优雅,一般来说比较优秀的代码是什么样的?
根据之前的解释,以函数为单位,个人觉得好的代码是一颗树,一颗主干分支分明、错落有致的树。入口函数可以看成树的主干,调用的函数是分支,树是逐渐细化的。如果单独看一个分支的话,它还是一颗树。
试想一下,没有分支、只有主干的树是否美观?主干和分支一样粗的树是否美观?一个不平衡的树是否美观?
6 不要过度封装
过度封装,反而把简单事情复杂化。
一旦过度封装, 直接的危害往往是写了无数行代码, 封装了N多个类, 就是看不出一个完整的功能, 因为分裂起来收不住. 即使最终把功能实现了, 代码维护性也让人不忍直视.
- 如何判断是否过度封装? 如何避免过度封装?
"直观优先"原则
前微软C#编辑器的开发主管Jay Bazuzi列出的一些有助于找到正确方向的问题;
他觉得前同事们应该用这些问题来问自己;实际上不管在哪里工作的开发者们都应该经常问问自己这些问题:
◆“要保证这个问题不会再出现,我该怎么做?”
◆“要想少出些Bug,我该怎么做?”
◆“要保证Bug容易被修复,我该怎么做?”
◆“要保持对变化的快速响应,我该怎么做?”
◆“要保证我的软件的运行速度,我该怎么做?”
如果大多数团队都能不时问一下自己,必定会从中得益,因为这些都是真正强而有力的问题。