软件设计是一个抽象的概念。它和程序的概括形状(Shape)、结构以及每一个模块、类和方法的详细形状和结构有关。可以使用许多不同的媒介去描绘它,但是它最终体现为源代码。最后,源代码就是设计。
拙劣设计的症状
- 僵化性(Rigidity):设计难以改变。很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其他改动。
- 脆弱性(Fragility):设计易于遭到破坏。对系统的改动会导致系统中和改动的地方在概念上无关的许多地方发现问题。
- 牢固性(Immobility):设计难以重用。很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
- 粘滞性(Viscosity):难以做正确的事情。做正确的事情比做错误的事情要困难。
- 不必要的复杂性(Needless Complexity):过分设计。设计中包含有不具有任何直接好处的基础结构。
- 不必要的重复性(Needlsee Repetition):滥用复制/粘贴。设计中包含有重复的结构,而该重复的对象本可以使用单一的抽象进行统一。
- 晦涩性(Opacity):很难阅读、理解。没有很好的表现出意图。
面向对象设计原则
- 单一职责原则(The Single Responsibility Principle,简称SRP)
- 开放-封闭原则(The Open-Close Principle,简称OCP)
- Liskov替换原则(The Liskov Subsititution Principle,简称LSP)
- 依赖倒置原则(The Dependency Inversion Principle,简称DIP)
- 接口隔离原则(The Interface Segregation Interface,简称ISP)
单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。
在SRP中,我们把职责定义为“变化的原因”。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的原则。
应用FACADE或PROXY模式对设计进行重构可以帮助分离类的职责。
开放—封闭原则(OCP)
软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。
遵循开放—封闭原则设计的模块具有两个主要的特征,他们是:
1、“对于扩展是开放的”(Open for extension)。
这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块的功能。
2、“对于更改是封闭的”(Closed for modification)。
对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者Java的“.jar”文件,都无需改动。
这两个特征好像是互相矛盾的。扩展模块的行为的通常方式就是修改该模块的源代码。不允许修改的模块常常被认为是具有固定的行为。
在面向对象的语言中,可以创建出固定却能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象基类。而这一组任意个可能的行为则可以表现为可能的派生类。
模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,所以它对于更改是可以关闭的。同时,通过这个抽象体派生,也可以扩展此模块的行为。
实际上使用抽象接口比使用抽象类通常具有更好的效果。
在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。然而,对于应用程序的 每个部分都肆意地进行抽象并不是一个好主意。应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要。
Liskov简单替换原则(LSP)
OCP背后的主要机制是抽象(abstraction)和多态(polymorphisim),支持抽象和多态的关键机制之一是继承。采用Liskov简单替换原则将保证系统使用了最佳的继承层次,正确使用继承不违反OCP。
子类型(subtype)必须能够替换掉他们的基类型(base type)
“这里需要如下替换性质:若对每个类型S的对象s1,都存在一个类型T的对象s2,使得在所有针对T编写的程序P中,用s1替换s2后,程序P行为功能不变,则S是T的子类型。”
——Barbara Liskov
LSP让我们得出一个非常重要的结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。
在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据该设计的使用者作出的合理假设来审视它。
1、基于契约设计
为了使“合理假设”明确化,更好支持LSP,可以采用基于契约设计(Design Contract,简称DBC)方法。
基于DBC方法,派生类的前置条件和后置条件规则是:
在重新声明的派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。
2、不易察觉的违反LSP的设计
a、 派生类的退化函数
派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。
b、从派生类抛出异常
如果在派生类的方法中添加了其基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。
LSP是保证OCP的重要原则。
依赖倒置原则(DIP)
高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
1、本质上倒置的应该是接口所有权。
低层模块实现了在高层模块中声明并被高层模块调用的接口。
2、依赖于抽象
程序中所有的依赖关系都应该终止于抽象类或接口。
任何变量都不应该持有一个具体类的指针和引用。
任何类都不应该从具体类派生。
任何方法都不应该覆写它的任何基类中已经实现的方法。
接口隔离原则(ISP)
不应该强迫客户依赖于它们不使用的方法。
一个对象的客户不是必须通过该对象的接口去访问它,也可以通过委托或者该对象的基类去访问它。
如何使用ISP
1、对客户进行分组
常常可以根据客户调用的服务方法来对客户进行分组。这种分组方法使得可以为每组而不是每个客户创建分离的接口。
2、改变接口
在维护面相对象的程序时,常常会改变现有的类和组件的接口。为了减少这些改变的影响。可以通过为现有的对象增加新接口的方法来缓解,而不是去改变现有的接口。
胖类(fat class)会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该类进行一个改动时,会影响到其他所有的客户程序。因此,客户程序应该依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定客户程序的接口,可以实现这个目标。