假如你想写本关于面向对象设计的书,你需要把这个大的课题拆分成一些小题目。把这些小题目分几个章节写,还得写前言,简介,说明,举例,一篇里还有很多段落。你需要设计一整本书,还得练习一些写作技巧,让文章读起来浅显易懂。这就是综观全局。
在软件开发中,OOD就是用来解决从全局出发考虑问题,在设计软件的时候,类和代码可以模块化,可重复使用,可灵活应用。
“一个敏捷设计的软件能轻松应对变化,能被扩展和复用”,而应用“面向对象设计”是做到敏捷设计的关键。
如果代码符合以下几点,那么你就在“面向对象设计”:
面向对象
复用
变化的代价极小
无需改代码即可扩展
现在有许多设计原则,但是最基本的,就是SOLID(缩写),这五项原则。
S = 单一责任原则
O = 开闭原则
L = Liscov替换原则
I = 接口隔离原则
D = 依赖倒置原则
“导致类变化的因素永远不要多于一个。”
或者换个说法:”一个类有且只有一个职责”。
如果有多于一个原因会导致你的类改变(或者它的职责多余一个),你就需要根据其职责把这个类拆分为多个类。
例子:
假如Rectangle 类干了下面两件事:
计算矩形面积;
在界面上绘制矩形;
而且,有两个程序使用了 Rectangle 类:
计算几何应用程序用这个类计算面积;
图形程序用这个类在界面上绘制矩形;
这违反了SRP原则(单一职责原则)!
导致的问题:要是为了图形应用而改变 Rectangle 类,计算几何应用也可能跟着变,然后还得编译,还得测试,另一边也是。
我们可以按职责把它拆成两个类:
Rectangle:这个类定义 Area() 方法;
RectangleUI:这个把 Rectangle 类继承过来,定义 Draw() 方法。
“软件实体(类,模块,函数等)应该对扩展开放,对修改关闭。”
这意味着在最基本的层面上,你可以扩展一个类的行为,而无需修改。这就像我能够穿上衣服,而对我的身体不做任何改变!
一个不支持 “开放-关闭” 原则的例子:
客户端类和服务端类都是具体的实现类。如果某些原因导致服务端实现改变了, 客户端也需要相应变化。
比如,如果一个浏览器的实现和一个指定的服务器(比如IIS)紧紧的耦合在一起 , 那么如果服务器由于某种原因替换成了另外的 (比如, Apache), 浏览器也需要做相应的变化或者被替换掉。
以下是一种好的设计方案:
在这个例子中, 添加了一个抽象的Server类, 并且客户端保持了抽象类的引用, 具体的Server类实现了这个抽象Server类. 所以, 由于某种原因Server的实现类发生了改变, 客户端不需要做任何改变.
这里的抽象的Server类对修改关闭, 具体的Server实现类对扩展开放.
“子类型必须能够替换它们的基类.”
或者, 换句话说:
“使用基类引用的函数必须能够使用派生类而无须了解派生类.”
在基本的面向对象原则中, “继承” 通常被描述成 “is a” 的关系. 如果一个 “开发者” 是”软件专业人员”, 那么 “开发者” 类 应该 继承 “软件开发人员” 类. 这样的 “Is a” 关系 在类设计阶段非常重要.
“里氏替换原则” 仅仅是一种确保继承被正确使用的手段.
一个例子:
类层次结构图展示的是一个Liskov替换原则的例子.因为 KingFisher类拓展(继承)了Bird类,因此继承了Fly()这个方法,这是非常不错的.
再看一个例子:
Ostrich(鸵鸟)是一种鸟,并继承了 Bird 类。但它能飞吗?不能,这个设计就违反了里氏替换原则。
因此,即使在现实中看上去没什么问题,在类设计中,Ostrich 都不应该继承 Bird 类,而应该从 Bird 中分出一个不会飞的类,由 Ostrich 继承。
总之:
如果不遵循 LSP原则,类继承就会混乱。如果子类实例被作为参数传递给方法,后果难以预测。
如果不遵循 LSP原则,基于父类编写的单元测试代码将无法成功运行子类。
“用户不应该被迫依赖他们不使用的接口。”
假如你有一些类,你通过接口暴露了类的功能,这样外部就能够知道类中可用的功能,客户端也可以根据接口来设计。当然,如果接口太大,或是暴露的方法太多,从外部看也会很混乱。接口包含的方法太多也会降低可复用性, 这种包含无用方法的”胖接口“无疑会增加类的耦合。
这还会引起其他的问题。如果一个类视图实现接口,它需要实现接口中所有的方法,哪怕一点都用不到。所以,这样会增加系统复杂度,降低系统可维护性和稳定性。
接口隔离原则确保接口实现自己的职责,且清晰明确,易于理解,具有可复用性。
下面的接口是一个“胖接口”,这违反接口隔离原则:
IBird接口定义 Fly()。现在,如果一只鸟(比方说,鸵鸟)实现了这个接口,它将要实现不必要的 Fly()的行为(鸵鸟不会飞)。
在这里,“胖接口”应该分隔成两个不同的接口——IBird 和IFlyingBird,而IFlyingBird继承于IBird。
“高层次的模块不应该依赖于低层次的模块,而是,都应该依赖于抽象。”
现实世界中,汽车是高层级的模块/实体,它依赖于底层级的模块/实体,例如引擎和轮子。
相较于直接依赖于实体的引擎或轮子,汽车应该依赖于抽象的引擎或轮子的规格,这样只要是符合这个抽象规格的引擎或轮子,都可以装到车里跑。
注意上面的 Car类,它有两个属性,且都是抽象类型(接口)而非实体的。
引擎和车轮是可插拔的,这样汽车能接受任何实现了声明接口的对象,且 Car 类无需任何改动。
如果代码不遵循依赖倒置,就有下面的风险:
使用低层级类会破环高层级代码;
当低层级的类变化时,需要太多时间和代价来修改高层级代码;
代码可复用性不高
除 SOLID 原则外还有很多别的面向对象原则。比如:
“组合替代继承”:是说“用组合比用继承好”;
“笛米特法则”:是说“类对其它类知道的越少越好”;
“共同封闭原则”:是说“相关类应该一起打包”;
“稳定抽象原则”:这是说"类越稳定,就越应该是抽象类";
参考:
How I explained OOD to my wife (http://www.codeproject.com/Articles/93369/How-I-explained-OOD-to-my-wife)
我是怎样教媳妇面向对象编程的(http://www.oschina.net/translate/how-i-explained-ood-to-my-wife)