原作者 Robert C. Martin
原文:http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf
引用页:http://www.objectmentor.com/resources/publishedArticles.html
二:面向对象设计原则
1 The Open-Close Principle(开闭原则)
A module should be open for extension but closed for modification.
一个模块应该对扩展开放,而对修改封闭。
这是所有OO设计原则中最重要的一条。我们应该构造这样的模块,使其不做修改即可被扩展。即我们可以在不改变模块源码的同时改变模块的行为。
多态是实现开闭原则最基本的方法。例子任何一本讲模式或者OO的书上都能找到,就不多举了。
2 The Liskov Substitution Principle(Liskov替换原则)
Subclasses should be substitutable for their base classes.Subclasses should be substitutable for their base classes.
子类应能够替代它们的基类。
这句话从语法上来说简直是句废话。在面向对象的语言中,对基类引用的场合都能够传入其子类,罕有例外。但在语义上就不一定了。看下面的例子。
class Rectangle
{
public:
void SetWidth(int width);
void SetHeight(int height);
int GetHeight();
int GetWidth();
}
void SquareTest(Rectangle& rect)
{
rect.SetWidth(5);
rect.SetHeight(6);
assert(rect.GetWidth() * rect.GetHeight() == 30);
}
上面这段测试代码无疑是任何时候都应该通过的。
从逻辑上讲,正方形是矩形的一种,是is-a的关系,因此从Rectangle类派生出Squre类似乎是顺理成章的事情。但如果上面SquareTest函数接受的是一个Square对象,assert一定会失败。这就违背了LSP。究其原因,在于Rectangle类隐含着如下假定:宽高可以被分别赋值而互不影响。
这里需要引入一个Design By Contract(按契约设计)来解释这个问题:LSP的本质是派生类必须满足基类所遵循的契约。上例中Rectangle隐含的契约:宽高可以被互不影响地赋值显然是Square做不到的。从契约角度讲,派生类必须做到如下两点:
1)前置条件不能强于基类。
2)后置条件不能弱于基类。
前置条件例:基类某个方法可以接受所有正数,而派生类覆盖了此方法,只接受大于10000的数,那么某个传1000给这个方法的调用者就会失败。
后置条件例:基类某方法返回偶数,而派生类覆盖此方法改为可返回奇数,那么对基类方法返回偶数进行校验的调用者就会失败。上例中的后置条件是设置高度不影响宽度,设置宽度不影响高度。但Square显然做不到这一点。
3 Dependency Inversion Principle (DIP)
控制反转原则
Depend Upon Abstractions. Do not depend on concretions.
如果OCP是OO的目标,那DIP就是主要的方法。DI是依赖于接口、抽象功能或抽象类的策略,而非依赖于具体功能 或类。这种原则是COM,CORBA,EJB等组件设计背后的力量。
过程式设计展现了一种特定的依赖结构。即自底向上构造软件,上层结构高度依赖于底层模块,而底层模块又依赖于更底层的模块。这种依赖关系导致了软件的脆弱。上层模块主要是处理上层策略,对下层如何实现并不关心。那为什么要上层模块直接依赖于底层实现呢?
而面向对象设计则展示了一种与此迥异的依赖关系。主要的依赖指向抽象。而具体实现这些抽象的模块则不再被依赖。相反,这些实现模块自身也依赖于抽象。
依赖抽象。依赖反转的含义很简单,所有的依赖都只应依赖于抽象,接口或抽象类。不应有任何依赖指向具体类。原因也很简单:具体类易变,而抽象类的改变则要少很多。此外,抽象类是占位符,它们代表了功能可以被改变或扩展,而抽象类自身不需更改的地方。