课程简介
就像修炼武学一般,编码的技能同样需要修行,只有掌握更多编码技能与设计技能的程序员才能在程序世界走得更远,攀得更高。掌握必备的设计技能,就好像是武者修行的洗髓炼气,决定了未来内力的强大;扎实的编码功底,则是修炼外功,打磨的是筋骨皮;至于开发工具、测试驱动、重构等诸多技能,则是编码武者掌握的招式,若能熟练掌握,就能一击制敌,让那些糟糕代码无容身之地。
故而,程序员能力的提升,就是编码武者的修行。
课程大纲
洗髓篇
设计心法
纵观软件开发的历史,其间经历了过程式设计、面向对象设计(函数式编程)、 面向组件设计、面向服务设计,然而无论是以什么内容作为驱动设计的要素,都 离不开设计的本原——“高内聚松耦合”。
这六字真言道尽了软件设计的终极目标:我们希望设计出来的实体(函数、类、 模块、子系统)可以重用,支持扩展,如此才能提高编码效率、减少系统缺陷, 同时还能面对需求的变化。
高内聚,意味着程序的职责分配合理,不会将相关的逻辑分散到各处,且又定义 了合理的边界,只暴露需要进行协作的接口;于是又引入了松耦合,使得实体之间明断实连,相依而不相存,可以独立变化,却又相互协作。
在本章,我将从多个角度来阐释、剖析这六字真言:
高内聚松耦合
设计的起点
重复谜题
对象的合理封装
自治对象
高内聚松耦合
内聚性即软件单位内部的关联紧密程度,这意味着在软件设计中,我们需要合理 分辨对象的职责,并让对象尽可能满足单一职责原则。而所谓“职责”,就是变化 原因,因而保持内聚性,就是要充分地识别变化。
耦合性指两个或多个软件单位之间的关联紧密程度。如果软件单位之间存在耦 合,就说明它会因为变化而产生影响,属于需要封装变化,保证API的抽象。
因而,高内聚松耦合原则推导出一个重要概念,即“变化”。而要有效地应对变 化,对内则需要高内聚,对外则需要保证松耦合。
对象的合理封装
对于对象的封装而言,我认为有两个步骤,可以帮助获得合理的封装。
首先是分辨职责,即弄清楚职责应该分配给什么对象。这个识别职责的过程同时 也是寻找对象的过程。在这个步骤中,可以尝试用一句话来描述职责,只要你描 述清楚了,则它应该依附的对象就应该自然而然显现。
第二步则是判别哪些是实现细节,那些是可以公开的接口,以保证对细节的合理 隐藏。暴露太多的细节可能会产生不合理的依赖,而从职责的角度来讲,这些细 节并不是调用者所关心的内容。
对象的封装应该遵循信息专家模式与迪米特法则,又或者从代码层面,识别出的 坏味道为Feature Envy。
案例分析:报表系统之参数处理
在我们的项目中,需要对客户发出的Web请求进行处理,获得我们需要的 参数。参数的值放在Request中,而我们事先已经根据配置文件,获得 了参数的类型信息。根据项目需要,我们将参数划分为三种:
单一参数(SimpleParameter);
元素项参数(ItemParameter);
表参数(TableParameter);
继承与委派的区别
继承是一种扩展机制,一种快速实现的重用手法。它在面向对象设计领域中的地 位举足轻重,却又经常被滥用。委派虽然失去了继承在重用方面的简单性和在多 态方面的优势,但却比继承更加地灵活,耦合度较低。一个良好的设计原则是优 先使用委派(即组合)而非继承。
案例分析:两种分离方案的对比
多态与抽象
抽象是保证可扩展设计的要素,同时,它也是多态的基础。多态是指一个对象在 不同时间表现为不同实例类型的能力。利用这种运行时可替换的特点,就可以根 据不同的条件,替换为不同的对象,实现功能的可替换,以满足功能的变化。
多态保证了程序的灵活性,因为它将对象形态的决定权交给了调用者。同时,它 还保证了程序的稳定性。由于抽象抹去了具体实现细节的差异,使得调用者不因 实现细节的变化而改变原来定义的抽象类型,从而达到了隔离变化的目的。
对于抽象而言,常常会与封装结合起来,分离变与不变,并对可能发生变化的部 分进行抽象。
扩展式设计
设计步骤可以分为:
识别变化点
封装变化
重复谜题
高内聚松耦合的目标是应对变化,而解决变化的首要条件是处理重复。一旦系统 中存在重复的知识(业务逻辑、解决方案),当知识发生变化时,与之相关的内 容就会受到影响。
业务逻辑的重复
针对业务逻辑的重复,解决的原则就是关注点分离。从OO思想看,设计在满足单 一职责原则的同时,还要遵循由Mayer提出的CQS原则;而结合FP思想,则是尽 可能地设计纯函数,将副作用无限制地往外推。
针对函数级别的设计,应追求层次化的分解,遵循单一抽象层次原则,这样的代 码就能做到不言自明,若有好的命名,读之若自然语言,并且更加精简准确。
若设计为纯函数,则可以利用组合子思想,对函数进行原子抽象,再通过组合的 方式满足具体的业务逻辑,能更加充分地满足重用。
案例分析:日志系统的组合子设计类级别的重用仍然遵循关注点分离原则,不同之处在于分离的方向。若分离方向为“向上”,则采用共性与可变性分析与差异式编程。若分离方向为“向外”,则满足扩展式设计,将两个(或多个)不同变化方向的职责分离。
案例分析:业务系统的数据库访问与事务处理,采用两种不同方式对其进 行重构,重构的结果是一个初步简略的框架。
程序结构的重复
在结构上完全相同,差异在于内部细节,且无法运用OO的参数或多态形式消除变化带来的影响。
案例分析:对集合的操作,运用函数式编程思想消除重复。
易筋篇
整洁之道
武功修行,内外兼修才是王道。软件设计同样如此,不能只炼心法(设计),而 缺少对身体(代码)的锤炼。二者(设计与代码)并非完全割裂的关系,而是相 辅相成,甚至内外相通的关系。好的设计可以在一定程度上保证好的编码,而把 握好整洁代码的特征,培养编码的Sense,则有助于改进设计的质量。
那么什么才是整洁代码呢?本章会对此展开探讨,以期端正编码者的态度,培养 良好编码习惯,打磨编码技能。
简单设计
Kent Beck提出了“简单设计的概念”,内容为:
通过所有测试(Passes its tests)
尽可能消除重复 (Minimizes duplication)
尽可能清晰表达 (Maximizes clarity)
更少代码元素 (Has fewer elements)
以上四个原则的重要程度依次降低。
案例:结合简单设计理解邮件转发器的设计
负重修行
外功修炼就是要负重而行。在这条修行道路上,我们需要突破如下内容的桎梏:
名:提高可读性的一方面
形:提高可读性的一方面
函数:构成程序的最重要元素
可读性
命名
可读性的一个关键是命名,要求对变量、函数、类以及模块等的命名能够很好地 传达设计者意图,让代码尽可能地清晰易懂。命名时,应充分考虑对各种词性的 运用,避免过长的名称,避免因为命名不当导致程序出现歧义。
表达式
不要将表达式的细节暴露在外,这会认为制造阅读代码的障碍,针对长表达式, 更需要对表达式进行合理封装。有时候,我们在使用条件判断时,可能用错了地 方,没有将真正存在差异的地方分辨出来。我们需要让判断条件做真正的选择。 要注意处理布尔型标志参数。
合理的分段
在编写代码时,通过对代码进行合理的分段,可以使得代码结构更加清晰,可读 性更高。分段时,也是对代码的理解,尤其是促进对职责的分辨。此时,可以考 虑标注合适的注释,使得代码更容易懂。同时,这种分段与注释,其实也是对代 码进行改造的基础。
DSL
DSL(Domain Specific Language),即领域特定语言。它通过运用一些模 式与方法,使得代码更好地体现领域知识,展现出让领域专家也能读懂的良好可 读性。
在编写代码时,我们最常使用的一个公共 DSL 模式,称之为“连贯接口
(Fluence Interface)”,他将其定义为能够为一系列方法调用中转或维 护指令上下文的行为。
案例分析:若干代码片段,多数来自于真实项目的丑陋代码,演示如何提 高这些代码的可读性,使得代码结构更加清晰。
整洁的函数
函数的第一规则是要短小。第二条规则是还要更短小。它应该遵循单一职责原 则。保证函数只做一件事,就可以有效地保证写出短小的函数,写出可读性高的 函数;同时也有利于函数的重用。前面的案例正是因为违背了这一原则,导致函 数变长,且不易理解。
阅读函数时,要注意识别和提取无关的子问题域,辨别函数的最高目标,从而改 进函数。应保证每个函数一个抽象层级。
案例分析:Fitness代码分析
异常处理
使用异常替代返回错误码。
采用错误码,就不可避免需要使用分支,从而可能导致更深层次的嵌套结构。其 次,若能使用异常来处理错误分支,则可以使得错误处理代码能够从主路径代码 中分离出来。
案例分析:版本升级管理系统的异常处理
重构
重构意即:在不修改原有功能的情况下改善代码。因此,在开发时,存在两项工 作:添加功能和重构。这两顶帽子应该频繁交换,但在同一时间只能戴一顶帽 子。
识别代码的坏味道,包括:重复的代码、过长的方法、过大的类、发散式变化、 霰弹式修改等。
使用IntelliJ IDEA的重构工具,对代码进行自动化重构。
案例实践:
影片租赁系统
分布式系统消息处理的测试
JBehave测试用例
综合思考
如何制定重构策略
代码不及时重构,就会让技术债越欠越多,最后导致整个系统的代码质量积重难 返,修改的成本越高,就越没有人愿意去改进代码,这就是所谓的“破窗理论”。
Code Review
进行Code Review的重要原则与实践
一个建议,可以成立一个代码诊所,把一些典型的代码问题晒出来,在团队活动 中进行分享(包括对Bug的复盘),在团队内形成良好的学习氛围。
案例:我在客户处做的代码诊所