参见:http://www.codeproject.com/Articles/93369/How-I-explained-OOD-to-my-wife
话题:为什么要进行面向对象设计?
软件开发唯一的真理是“软件必然修改”。一个敏捷设计的软件能轻松应对变化,能被扩展和复用。而应用“面向对象设计”是做到敏捷设计的关键。如果代码符合以下几点,那么你就在“面向对象设计”:
- 面向对象
- 复用
- 变化的代价极小
- 无需改代码即可扩展
现在有许多设计原则,但是最基本的,就是SOLID(缩写),这五项原则。
S = 单一责任原则
O = 开闭原则
L = Liscov替换原则
I = 接口隔离原则
D = 依赖倒置原则
话题:单一功能原则
从面向对象角度解释是:
"导致类变化的因素永远不要多于一个。"
或者换行个说法:"一个类有且只有一个职责"。
当然可以在一个类中包含多个方法。问题是,他们都是为了一个目的。那么,为什么拆分很重要的?
那是因为:
- 每个职责都是轴向变化;
- 如果类包含多个职责,代码会变得耦合;
见下面的例子:
这里,Rectangle 类干了下面两件事:
- 计算矩形面积;
- 在界面上绘制矩形;
而且,有两个程序使用了 Rectangle 类:
- 计算几何应用程序用这个类计算面积;
- 图形程序用这个类在界面上绘制矩形;
这违反了SRP原则(单一职责原则)!
需要按职责拆成两个类:
- Rectangle:这个类定义 Area() 方法;
- RectangleUI:这个把 Rectangle 类继承过来,定义 Draw() 方法。
SPR 就是把东西分到不能再分了,再集中化管理和复用。
方法也得分开,一个方法干一个活。这么着你复用方法,要是改了,也不用改太多。
话题:开闭原则
“软件实体(类,模块,函数等)应该对扩展开放,对修改关闭。”
这意味着在最基本的层面上,你可以扩展一个类的行为,而无需修改。这就像我能够穿上衣服,而对我的身体不做任何改变。
这个就不支持 "开放-关闭" 原则:
客户端类和服务端类都是具体的实现类. 因为, 如果某些原因导致服务端实现改变了, 客户端也需要相应变化.
下面的将是一种好的设计方案:
在这个例子中, 添加了一个抽象的Server类, 并且客户端保持了抽象类的引用, 具体的Server类实现了这个抽象Server类. 所以, 由于某种原因Server的实现类发生了改变, 客户端不需要做任何改变.
这里的抽象的Server类对修改关闭, 具体的Server实现类对扩展开放.
基本上, 你要对系统的核心业务进行抽象, 如果你抽象化做的比较好, 很可能, 在扩展功能的时候它们不必做任何改变 (比如Server就是一个抽象的概念). 你所定义的抽象的实现 (比如, IIS服务器 实现了 Server) 和 抽象的代码 (Server) 要尽可能的多. 这样在客户端代码中不需要做任何修改就会允许你定义一个新的实现(比如, ApacheServer) .
主题: 里氏替换原则
"子类型必须能够替换它们的基类."
或者, 换句话说:
"使用基类引用的函数必须能够使用派生类而无须了解派生类."
Ostrich(鸵鸟)是一种鸟(显然是),并继承了 Bird 类。但它能飞吗?不能,这个设计就违反了里氏替换原则。
因此,即使在现实中看上去没什么问题,在类设计中,Ostrich 都不应该继承 Bird 类,而应该从 Bird 中分出一个不会飞的类,由 Ostrich 继承。
- 如果不遵循 LSP原则,类继承就会混乱。如果子类实例被作为参数传递给方法,后果难以预测。
- 如果不遵循 LSP原则,基于父类编写的单元测试代码将无法成功运行子类。
话题:接口隔离原则
“用户不应该被迫依赖他们不使用的接口。”
假如你有一些类,你通过接口暴露了类的功能,这样外部就能够知道类中可用的功能,客户端也可以根据接口来设计。当然那,如果接口太大,或是暴露的方法太多,从外部看也会很混乱。接口包含的方法太多也会降低可复用性, 这种包含无用方法的”胖接口“无疑会增加类的耦合。
这还会引起其他的问题。如果一个类视图实现接口,它需要实现接口中所有的方法,哪怕一点都用不到。所以,这样会增加系统复杂度,降低系统可维护性和稳定性。
接口隔离原则确保接口实现自己的职责,且清晰明确,易于理解,具有可复用性。
下面的接口是一个“胖接口”,这违反接口隔离原则:
IBird接口定义 Fly()的行为有许多鸟类的行为。现在,如果一只鸟类(比方说,鸵鸟)实现了这个接口,它将会实现不必要的 Fly()的行为(鸵鸟不会飞)。
“胖接口”应该分隔成两个不同的接口,IBird 和IFlyingBird,而IFlyingBird继承于IBird。
接口隔离原则的例子中正确版本的接口
如果有一只不会飞的鸟(比如,驼鸟),只要用IBird接口即可,如果有一保会飞的鸟(比如,翠鸟),只要用IFlyingBird接口即可。
若是他们真的想要复用这个方案,他们应该将方案分为更小的部分,才能在以后制造新产品的时候复用这些设计方案。
话题:依赖倒置原则
“高层次的模块不应该依赖于低层次的模块,而是,都应该依赖于抽象。”
注意上面的 Car类,它有两个属性,且都是抽象类型(接口)而非实体的。
引擎和车轮是可插拔的,这样汽车能接受任何实现了声明接口的对象,且 Car 类无需任何改动。
若不满足这点:
- 使用低层级类会破环高层级代码;
- 当低层级的类变化时,需要太多时间和代价来修改高层级代码;
- 代码可复用性不高
总结
- “组合替代继承”:是说“用组合比用继承好”;
- “笛米特法则”:是说“类对其它类知道的越少越好”;
- “共同封闭原则”:是说“相关类应该一起打包”;
- “稳定抽象原则”:这是说"类越稳定,就越应该是抽象类";
设计模式是“框架”,OOD 原则是“规范”。