敏捷设计
前言
本系列内容为 Robert C. Martin 敏捷开发一书的读书笔记,前两篇介绍了敏捷开发的一些基本原则和方法,这一篇开始介绍 敏捷设计。
一、 什么是敏捷设计
1. 设计的臭味--腐化软件的气味
① 僵化性: 很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其他改动。
② 脆弱性: 对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
③ 牢固性: 很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
④ 粘滞性: 做正确的事情比做错误的事情要困难。
⑤ 不必要的复杂性: 设计中包含有不具备任何直接好处的基础结构。
⑥ 不必要的重复: 设计中包含有本可以用单一的抽象进行统一的重复结构。
⑦ 晦涩性: 很难阅读、理解。没有很好的表现出意图。
敏捷在这方面的优势是 团队几乎不进行预先设计,而是愿意保持系统设计尽可能的干净、简单,并使用许多单元测试和验收测试作为支援。利用这种灵活性,持续的改进设计。
2. 如何避免
敏捷开发人员有三个准则:
① 他们遵循敏捷实践去发现问题;
② 他们应用设计原则去诊断问题;并且
③ 他们应用适当的设计模式去解决问题。
设计必须要保持干净、简单,源代码同样要时刻保持干净。作为软件开发人员,不能忍受代码腐化。
3. 结论
敏捷设计是一个过程,是一个持续的应用原则、模式以及实践来改进软件的结构和可读性的过程。
二、 单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。
1. 什么是职责
在SRP中,我们把职责定义为“变化的原因”。如果你能想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。
interface Modem
{
public void dial(String pno);
public void hangup();
public void send(char c);
public void recv();
}
* 根据需求权衡Modem中的四个方法,是否需要分离。连接和发送接收,一般认为是两个职责。
2. 与持久化的耦合
业务规则和持久化这两个职责在大多数情况下绝不应该混合在一起。业务规则往往会频繁的变化,而持久化的方式往往比较稳定,即使变化也是跟业务有不同的原因。
对于现有项目中的于持久化的耦合,应该考虑使用FACADE和PROXY模式对设计进行重构,分离这个职责。
3. 结论
SRP是所有原则中最简单的之一,也是最难正确运用的之一。
三、 开放-封闭原则(OCP)
软件实体(类、模块、函数等)应该是可以扩展,但是不可修改的。
1. 解释
遵循开发-封闭原则涉及出的模块具有两个主要的特征: 对扩展开放,对更改封闭。
2. 方法--使用抽象
模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,所以它对于更改可以是关闭的。同时,通过从这个抽象体派生,可以扩展此模块的行为。
3. 实际应用
通常,我们更愿意一直等到确实需要那些抽象时再把它放置进去。
① 只受一次愚弄:变化发生时,我们就创建抽象来隔离以后发生的同类变化。
② 刺激变化:通过 首先编写测试、很短的迭代周期、加入基础结构前就开发特性、尽早的经常性的发布软件 等方式来刺激变化的发生。
4. 使用抽象获得显示封闭
通过一种“顺序抽象体”中的抽象接口,可以表示任何可能的排序策略。从而让DrawAllShapes对于绘制顺序的变化是封闭的。
class Shape
{
public:
virtual void Draw() const = 0;
virtual bool Precedes(const Shape&) const = 0;
bool operator<(const Shape& s) {return Precedes(s); }
};
5. 使用“数据驱动”的方法获取封闭性
const char* Shape::typeOrderTable[] =
{
typeid(Circle).name(),
typeid(Square).name(),
0
};
6. 结论
开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。
四、 Liskov 替换原则(LSP)
子类型(subtype)必须能够替换掉它们的基类型(base type)。
1. 一个违反LSP的简单例子
对于LSP的违反常常会导致以明显违反OCP的方式使用运行时类型辨别(RTTI)。常常是使用一个显式的if语句或if/else链去确定一个对象的类型。
void DrawShape(const Shape& s)
{
if (s.itsType == Shape::square)
static_cast<const Square&>(s).Draw();
else if (s.itsType == Shape::circle)
static_cast<const Circle&>(s).Draw();
}
2. 正方形和矩形--更微妙的违规
一般意义上来说,一个正方形就是一个矩形。是符合is-a关系的。但是实际应用中也确实是有问题的。
LSP让我们得出一个非常重要的结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。
IS-A是关于行为的。正方形和矩形,从对象的行为方式上来说是不相容的。
有一项技术叫 基于契约的设计(Design By Contract):类的编写者显式的规定针对该类的契约。客户代码的编写者可以通过该契约获悉可以依赖的行为方式。
契约是通过为每个方法声明的前置条件和后置条件来指定的。
还可以通过编写单元测试的方式来指定契约。
3. 用提取公共部分的方法代替继承
如果一组类都支持一个公共的职责,那么它们应该从一个公共的父类继承该职责。
4. 启发式规则和习惯用法
派生类中的退化函数 和 从派生类中抛出基类不会抛出的异常 也是可能违反LSP的。
5. 结论
OCP是OOD中很多说法的核心,而LSP是使OCP成为可能的主要原则之一。
五、 依赖倒置原则(DIP)
a. 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
b. 抽象不应该依赖于细节。细节应该依赖于抽象。
1. 层次化
所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的,受控的接口向外提供了一组内聚的服务。
2. 高层策略
高层策略是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统,它是隐喻( metaphore)。
六、 接口隔离原则(ISP)
不应该强迫客户依赖于它们不用的方法。
1. 接口污染
2. 分离客户就是分离接口
3. 类接口与对象接口
两个需要操作同样数据的接口,必须在同一个对象中实现。这种情况如何遵循ISP呢?
① 使用委托分离接口
② 使用多重继承分离接口
七、 总结
本章分别介绍了面向对象设计中的五个核心原则。
单一职责原则(SRP)、开放-封闭原则(OCP)、Liskov 替换原则(LSP)、 依赖倒置原则(DIP)、接口隔离原则(ISP)。