在敏捷团队中,全局视图和软件一起演化。在每次迭代中,团队改进系统设计,使设计尽可能适合于当前系统。团队不会花费更多时间去预测未来的需求和需要。也不构建一些基础结构去支撑可能明天才回需要的特性。他们更关注当前的系统结构,并使它尽可能地好。
拙劣设计的症状
- 僵化性(Rigidity):设计难以改变
- 脆弱性(Fragility):设计易于遭到破坏
- 牢固性(Immobility):设计难以重用
- 粘滞性(Viscosity):难以做正确的事情
- 不必要的复杂(Needless Complexity):过分设计
- 不惜要的重复(Needless Repetition):滥用鼠标
- 晦涩性(Opacity):混乱的表达
原则
- 单一职责原则(The Single Responsibility Principle, SRP)
- 开放-封闭原则(The Open-Close Principle, OCP)
- Liskov替换原则(The Liskov Substitution Principle, LSP)
- 依赖倒置原则(The Dependency Inversion Principle, DIP)
- 接口隔离原则(The Interface Segregation Interface, ISP)
设计中的臭味是可以主观进行度量的。是违反了原则中的一个或多个而导致的。敏捷团队应该应用这些原则来除去臭味。当没有臭味时,不应用这些原则。
第七章 什么是敏捷设计
本章作者提出了“源代码就是设计”的观点。接下来逐条论述了软件中的7条设计臭味:
- 僵化性(Rigidity):很难对系统进行改动,因为每个改动都会迫使许多对系统其它部分的其他改动。
- 脆弱性(Fragility):对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
- 牢固性(Immobility):很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
- 粘滞性(Viscosity):做正确的事情比做错误的事情要困难。
- 不必要的复杂(Needless Complexity):设计中包含有不具任何直接好处的基础结构。
- 不惜要的重复(Needless Repetition):设计中包含有重复的结构,而该重复的结构可以使用单一的抽象进行统一。
- 晦涩性(Opacity):很难阅读、理解。没有很好地表达出意图。
1. 僵化性
僵化性:指难以对软件进行改动过,即使是简单的改动。如果单一的改动晦导致有依赖关系的模块中的连锁改动,那设计就是僵化的。必须要改进的模块越多,设计就越僵化。
2. 脆弱性
脆弱性:在进行一个改动时,程序的许多地方就可能出现问题。常常是,出现新问题的地方与改动的地方并没有概念上的关联。要修正这些问题就又会引出更多的问题。
3. 牢固性
牢固性:设计中包含了对其他系统有用的部分,但是要把这些部分从系统中分离出来所需要的努力和风险是巨大的。
4. 粘滞性
粘滞性有两种表现:软件的粘滞性和环境的粘滞性:
当面临一个改动时,常常发现会有多种改动的方法。其中,一些方法晦保持设计;而另外一些破坏设计。
5. 不必要的复杂性
设计中包含有当前没用的组成部分,含有不必要的复杂性。
6. 不必要的重复
当同样的代码以稍微不同的形式一再出现时,就表示开发人员忽视了抽象。
7. 晦涩性
晦涩性:指模块难以理解。
总之,敏捷团队依靠变化来获取活力。团队几乎不进行预先(Up-front)设计,不需要预先设计一个成熟的初始设计。要保持系统设计尽可能的干净、简单,并使用许多单元测试和验收测试作为支援。
敏捷开发人员致力于保持设计尽可能地适当、干净。
什么是敏捷设计?
敏捷设计是一个过程,不是一个时间。它是一个持续的应用原则、模式以及实践来改进软件的结构和可读性的过程。
敏捷开发人员不会对一个庞大的预先设计应用那些原则和模式。这些原则和模式被应用在一次次的迭代中,力图使代码以及代码所表达的设计保持干净。
第八章 单一职责原则(SRP)
如果一个类承担的职责过多,等于把这些职责耦合在一起。耦合会导致脆弱的设计。
在SRP中,把职责定义为“变化的原因(A reason for change)”,当一个类同时包含了业务规则和对持久化的控制时,应使职责分离,应使用** Facade 或 Proxy** 模式对设计进行重构,分离这两个职责。
第九章 开放-封闭原则(OCP)
OCP的主要机制 - ** 抽象和多态 。在JAVA中,支持抽象和多态的关键机制是 继承 **。用继承,创建实现其基类中抽象方法的派生类。
软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。
如果设计具有僵化性的臭味。OCP建议应对系统进行重构。
遵循开放-封闭原则设计出的模块具有两个主要的特征:
- 对于扩展是开放的(Open for extension)- 模块的行为是可以扩展的;
- 对于更改是封闭的(Closed for modification)- 对模块行为进行扩展时,不必改动模块的源代码或二进制代码。
关键是抽象
模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体。通过从这个抽象体派生,也可以扩张此模块的行为。
STRATEGY模式:即开放又封闭的类。
Template Method模式:即开放又封闭的基类。
通过Strategy,Template Method两个模式是满足OCP的最常用的方法。把一个功能的通用部分和实现细节部分清晰的分离开来。
无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。设计人员必须对于他设计的模块应该对哪种编程封闭做出选择。需先猜测最有可能发生的变化种类,然后构造抽象来隔离那些变化。
抽象会增加了软件设计的复杂性。
封闭是建立在抽象基础上的。
第十章 Liskov替代原则(LSP)
对于LSP做如下解释:子类型必须能够替代掉它们的基类型。
对于违反LSP指 - 导致以明显违反OCP的方式使用运行时类型辨别(RTTI)。
继承是IS-A(“是一个”)关系。
如果新派生类的创建会导致改变基类,这意味着设计是有缺陷的,违反了OCP。
LSP:一个模型,如果孤立地看,并不具有真正意义上的有效性。模块的有效性只能通过它的客户程序来表现。
IS-A:是关于行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。
基于契约设计(Design By Contract, DBC):使合理的假设明确化。使用DBC,类的编写者显示地针对该类的契约。契约是通过为每个方法声明的前置条件(Preconditions)和后置条件(Postconditions)来制定的。要一个方法得以执行,前置条件必须为真。执行完毕后,该方法保证后置条件为真。
派生类的前置条件和后置条件规则是:
在重新声明派生类中的例程(Routine)时,只能使用相等或更弱的前置条件来替代原始的前置条件,只能使用相等或更强的后置条件来替代原始的后置条件。
派生类必须和基类的所有后置条件一致。
另一个种LSP的违规形式:在派生类的方法中添加了其基类不会跑出的异常。
第十一章 依赖倒置原则(DIP)
- a. 高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
- b. 抽象不应该依赖于细节。细节应该依赖于抽象。
高层业务规则的模块应优先并独立于包含实现细节的模块。
层次化
结构良好的架构都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供了一组内聚的服务。
每个较高层次都为它所需要的服务声明一个抽象接口,较低的层次实现了这些抽象接口,每个高层类都通过该抽象接口使用下一层,这样高层就不依赖于低层。低层反而依赖于在高层中声明的抽象服务接口。
倒置不仅仅是依赖关系的倒置,也是接口所有权的倒置。应用了DIP时,往往客户拥有抽象接口,服务者则从这些抽象接口派生。
不应该依赖于具体类,程序中所有的依赖关系都应该终止于抽象类或接口。
根据启发式规则:
- 任何变量都不应该持有一个定向具体类的指针或引用;
- 任何类都不应该从具体类派生;
- 任何方法都不应该覆写它的任何基类中的意境实现了方法。
第十二章 接口隔离原则(ISP)
通过ISP来处理“胖(Fat)”接口所具有的缺点。
如果接口不是内聚的(Cohensive),就表示该类具有“胖”的接口。这种接口类可分解成多组方法,每一组方法都服务于一组不同的客户程序。
胖类:会导致它们的客户程序之间产生不正常的并且有害的耦合关系。
通过把胖类的接口分解为多个特定于客户程序的接口,可以实现ISP的这个目标。
ISP:不应该强迫客户依赖他们不用的方法。
一个对象的客户不是必须通过该对象的接口去访问它,也可以通过委托或通过该对象的基类去访问它。
可以使用** 多重继承 ** 来达到符合ISP的目标。
网友评论