前置介绍
Kent Beck(肯特·贝克)
设计模式
重构
《重构:改善既有代码的设计》,代码坏味道
极限编程
一个轻量级的、灵巧的软件开发方法;同时它也是一个非常严谨和周密的方法。它的基础和价值观是交流、朴素、反馈和勇气;即,任何一个软件项目都可以从四个方面入手进行改善:加强交流;从简单做起;寻求反馈;勇于实事求是。XP是一种近螺旋式的开发方法,它将复杂的开发过程分解为一个个相对比较简单的小周期;通过积极的交流、反馈以及其它一系列的方法,开发人员和客户可以非常清楚开发进度、变化、待解决的问题和潜在的困难等,并根据实际情况及时地调整开发过程。
敏捷软件开发的一种,极限编程和传统方法学的本质不同在于它更强调可适应性以及面临的困难。
测试驱动开发
英文全称Test-Driven Development,简称TDD
简单设计原则
Kent Beck给出的答案:
- 通过所有测试(Passes its tests)
- 尽可能消除重复 (Minimizes duplication)
- 尽可能清晰表达 (Maximizes clarity)
- 更少代码元素 (Has fewer elements)
以上四个原则的重要程度依次降低。
这组定义被称做简单设计原则。
1、通过所有测试(实现功能)
这里提到的测试,真正的意思是客户验收。如果你的项目通过了客户的所有验收条件(Acceptance Criteria),那就说明你们已经完成与客户约定的全部需求。至于验收方式是靠人工还是靠自动化测试则无关紧要。
所以,这句话强调的是对外部需求——包括功能性需求和非功能性需求——正确的完成。
2、尽可能消除重复(易于重用)
重复,意味着低内聚,高耦合。而消除重复的过程,也就意味着是让软件走向高内聚,低耦合,达到良好正交性的过程。识别和消除重复,对于增强软件应对变化能力的重要程度,怎么强调都不为过。
不过,并不是所有的重复都可以消除:比如,C++一个源文件里对外部公开的类,其每个public方法原型,除了在源文件里定义时,需要声明一次,还需要在头文件里再次声明。这样的重复,是语言机制的要求,无法消除。
因而,这条原则被描述为最小化重复,而不是消除重复。
3、尽可能清晰表达(易于理解)
清晰性,指的是一个设计容易理解的程度。注意:这不仅仅是对整洁代码(Clean Code)及声明式设计(Declarative Design)的强调。
#### 声明式设计
声明式设计,描述想要让一个事物达到的目标状态,由这个工具自己内部去处理如何令这个事物达到目标状态。
命令式设计或者过程式设计,描述的是一系列的动作,这一系列的动作如果被正确执行,最终结果将达到我们期望的目标状态。
声明式设计和命令式设计的区别
声明式设计,是告诉计算机你想要什么,由计算机自己去设计执行路径,如SQL;
命令式设计,是直接向计算机发出服务器要执行的操作命令;
相比于更关注状态和结果的声明式设计方式,命令式设计方式更强调过程。
幻数
magic number,弊端:代码可读性差,修改不方便
4、更少代码元素(没有冗余)
这一条是点睛之笔,正是因为它的存在,这组原则才被称做简单设计原则,从而区别于其它设计原则。
在这里,常量,变量,函数,类,包 …… 都属于代码元素。代码元素的数量,通常反映了设计的复杂度。因而,这句话强调的是:尽可能降低复杂度,保持简单。
总结
简单设计原则,通过对需求、易修改性、可理解性、复杂度,这四个在设计决策中最关键的因素给出了排序,让简单设计不再一个语义模糊的口号,而是对设计决策给出了清晰判断标准。
正交设计
「正交」是一个数学概念:所谓正交,就是指两个向量的内积为零。简单的说,就是这两个向量是垂直的。在一个正交系统里,沿着一个方向的变化,其另外一个方向不会发生变化。为此,Bob
大叔将「职责」定义为「变化的原因」。
「正交性」,意味着更高的内聚,更低的耦合。为此,正交性可以用于衡量系统的可重用性。
1、一个出发点
软件设计的目的只有一个:功能实现。这是一个软件存在的根本原因。
但随着软件越来复杂,单一过程的复杂度已经超出掌控极限。这逼迫人们必须对大问题进行分解,分而治之。
这就是模块化设计的最初动机。
2、两个问题
一旦开始进行进行模块化拆分,就必须解决如下两个问题:
- 究竟软件模块该怎样划分才是合理的?
- 将一个大单元划分为多个小单元之后,它们之间必然要通过衔接点进行合作。如果我们把这些衔接点看作API,那么问题就变为:怎样定义API才是合理的?
更简单的说:怎么分?然后再怎么合?
而这两个问题的答案,正是现代软件设计的核心关注点。
3、三方关系
为了找到这两个问题的答案,我们需要重新回到最初的问题:为何要做软件设计?
Kent Beck给出的答案是:软件设计是为了让软件在长期范围内容易应对变化。
在这个精炼的定义中,包含着三个关键词:长期,容易,变化。这意味着:
- 越是需要长期维护的项目,变化更多,也更难预测变化的方式;
- 软件设计,事关成本;
- 如何在难以预测的千变万化中,保持低廉的变更成本,正是软件设计要解决的问题。
对此,Kent Beck提出了一个更为精炼的原则:局部化影响。意思是说,我们希望,任何一个变化,对于我们当前的软件设计影响范围都可以控制在一个尽量小的局部。
如何才能做到?
内聚与耦合
一个易于应对变化的软件设计应该遵从高内聚,低耦合原则。
内聚性:关注的是一个软件单位内部的关联紧密程度。因而高内聚追求的是关联紧密的事物应该被放在一起,并且只有关联紧密的事物才应该被放在一起。简单说,就是Unix的设计哲学:
Do One Thing, Do It Well。
耦合性:强调两个或多个软件单位之间的关联紧密程度。因而低耦合追求的是,软件单位之间尽可能不要相互影响。
对应最初的两个问题:
- 当我们划分模块时,要让每个模块都尽可能高内聚;
- 而当我们定义模块之间的API时,需要让双方尽可能低耦合。
除了内聚与耦合之外,上面这幅图还揭示了另外一种关系:正交。具备正交关系的两个模块,可以做到一方的变化不会影响另外一方的变化。换句话说,双方各自独自变化,互不影响。
这幅图的右侧,正是我们模块化的目标。它描述了永恒的三方关系:客户,API,实现,以及它们之间的关系。这个三方关系图清晰的指出了我们应该关注的内聚性,耦合性,以及正交性都发生在何处。
4、四个策略
策略一:消除重复
对于完全重复的代码进行消除,合二为一,会让系统更加高内聚、低耦合。
对于模块之间存在部分重复的。如下图所示,两个模块存在着部分重复。站在系统的角度看,它们之间存在着不变的部分(即重复的部分);也存在变化的部分(即差异的部分)。这意味着这两个模块都存在两个变化原因或两重职责。
对于这一类型的重复,比较典型的情况有两种:调用型重复,以及回调型重复。它们的命名来源于:在重复消除后,重复与差异之间的关系是调用,还是回调。
回调型重复的消除,也是一个提高系统可扩展性的过程。
分离不同变化方向
分离不同变化方向,目标在于提高内聚度。因为多个变化方向,意味着一个模块存在多重职责。将不同的变化方向进行分离,也意味着各个变化方向职责的单一化。
对于变化方向的分离,也得到了另外一个追求的目标:可扩展性。
消除重复和分离不同变化方向是两个高度相似和关联的策略:
它们都是关注于如何对原有模块进行拆分,以提高系统的内聚性。(虽然同时也往往伴随着耦合度的降低,但这些耦合度的降低都发生在别处,并未触及该如何定义API以降低客户与API之间耦合度)。
消除重复:两个模块内有重复代码。
分离不同变化方向:同一个模块内,有不同分支操作。
缩小依赖范围
前面两个策略解决了软件单元该如何划分的问题。现在关注模块之间的粘合点——即API——的定义问题。
需要强调的是:两个模块之间并不存在耦合,它们的都共同耦合在API上。因而 API如何定义才能降低耦合度,才是我们应该关注的重点。
从这幅图可以看出,对于API定义所带来的耦合度影响,需要遵循如下原则:
- 首先,客户和实现模块的数量,会对耦合度产生重大的影响。它们数量越多,意味着 API 变更的成本越高,越需要花更大的精力来仔细斟酌。
- 其次,对于影响面大的API(也意味着耦合度高),需要使用更加弹性的API定义框架,以有利于向前兼容性。
而具体到策略缩小依赖范围,它强调:
- API 应包含尽可能少的知识。因为任何一项知识的变化都会导致双方的变化;
- API 也应该高内聚,而不应该强迫API的客户依赖它不需要的东西。
向稳定的方向依赖
耦合的最大问题在于:耦合点的变化,会导致依赖方跟着变化。但这也意味着,如果耦合点从来不会变化,那么依赖方也就不会因此而变化。换句话说,耦合点越稳定,依赖方受耦合变化影响的概率就越低。
由此,得到最后一个策略:向着稳定的方向依赖。
那么,究竟什么样的API更倾向于稳定?不难知道,站在What,而不是How的角度;即 站在需求的角度,而不是实现方式的角度定义API,会让其更加稳定。
而需求的提出方,一定是客户端,而不是实现侧。这就意味着,我们在定义接口时,应该站在客户的角度,思考用户的本质需要,由此来定义API。而不是站在技术实现的方便程度角度来思考API定义。
而这正是封装或信息隐藏的关键。
这四个策略,前两者聚焦于如何划分模块,后两个聚焦于如何定义模块间的API。
这四个策略的背后动力非常明确:变化驱动。前两者,都是在明确的变化方向被第一次识别之后,进行策略运用,以让模块在变化面前越来越高内聚。而后两者,则是在模块职责分离之后,需要定义模块间API时,尽可能考虑不同的API定义方式对于依赖双方的影响。
由于这四个策略致力于让系统朝着更具正交性的方向演进,因而它们也被称做正交四原则,或者正交策略。
总结
先从一个出发点出发:为了降低软件复杂度,提升可重用性,我们需要模块化。
由此得到了两个问题:模块划分必然要解决如何划分,以及模块间如何协作(API 定义)的问题。
基于软件易于应对变化的角度出发。高内聚,低耦合原则是最为核心和关键的高层原则。基于此我们得到了在模块化过程中,我们真正需要关注的三方关系。
为了让高内聚、低耦合更具指导性和操作性,我们提出了四个策略。它们以变化驱动,让系统逐步向更好的正交性演进的策略,因此也被称做正交策略或正交原则。
正交设计与SOLID
正交设计,是普遍的设计原则,与粒度无关,与编程范式无关,更与具体的实现语言无关。
- 软件模块该如何划分?
- 模块间API该如何定义?
软件模块划分应该以基于信息隐藏为目的,以职责划分为手段,从而封装变化,让软件更加容易修改(即Kent Beck
的理想:局部化影响)。
变化及应对变化,是软件设计最大的挑战,目的和意义。
SOLID
面向对象设计五个基本原则:单一职责、开放封闭、里氏替换、接口隔离、依赖倒置
正交原则与SOLID的关系
单一职责和开放封闭,更多的在强调类划分时的高内聚;而里氏替换,依赖倒置,接口隔离则更多的强调类与类之间协作接口(即API)定义的低耦合。
单一职责
单一职责,通过对变化原因的识别,将一个承担多重职责的类,不断分割为更小的,只具备单一变化原因的类。单一职责原则本身,并没有明确指示我们该如何判定一个类属于单一职责的,以及如何达到单一职责的状态。而策略消除重复,分离不同变化方向,正是让类达到单一职责的策略与途径。
开放封闭
开放封闭原则,正是通过将不同变化方向进行分离,从而达到对于已经出现的变化方向,对于修改是封闭的,对于扩展是开放的。
里氏替换
里氏替换原则强调的是,一个子类不应该破坏其父类与客户之间的契约。唯有如此,才能保证:客户与其父类所暴露的接口(即API)所产生的依赖关系是稳定的。子类只应该成为隐藏在API背后的某种具体实现方式。
依赖倒置
依赖倒置原则则强调:为了让依赖关系是稳定的,不应该由实现侧根据自己的技术实现方式定义接口,然后强迫上层(即客户)依赖这种不稳定的API定义,而是应该站在上层(即客户)的角度去定义API(正所谓依赖倒置)。
但是,虽然接口由上层定义,但最终接口的实现却依然由下层完成,因此依赖倒置描述为:上层不依赖下层,下层也不依赖上层,双方共同依赖于抽象。
接口隔离
接口隔离原则强调的是:不应该强迫客户依赖它不需要的东西。显然,这是缩小依赖范围策略在面向对象范式下的产物。
总结
正交设计是一种与范式,语言无关的设计原则。为了解决在模块化的过程中,如何让软件在长期范围内更容易应对变化。
而面向对象是一种对于模块化进行良好支持的范式。通过高内聚,低耦合原则,或正交策略的运用,面向对象范式下SOLID
原则会自然的浮现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。