面向对象设计原则
单一职责原则
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。
单一职责原则(Single Responsibility Principle)定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
我感觉这个原则比较难应用:单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构的经验。
开闭原则
开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。
开闭原则(Open-Closed Principle)定义:一个软件实体应当对扩展开放,对修改关闭。也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为。
我觉得将这个原则称之为最重要的设计原则不为过。设想在软件需求变更时,如果只需要在现有的内容上添加模块,不必在意之前写的代码(不用考虑之前代码诸多的细节),减少了耦合性,这该多好。
下面通过举例来说明开闭原则:
某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始设计方案如下:
如果界面类需要将圆形按钮改为矩形按钮,则需要修改LoginForm类的源代码,修改按钮类的类名。由于圆形按钮和矩形按钮的显示方法不相同,因此还需要修改LoginForm类中display()方法的实现代码。可以看到举的例子还只是一个小例子,就涉及到了两处更改;在实际生活中真遇到耦合性大的情况,如果代码是自己写的,那修改还比较轻松(也可能已经忘了之前写过什么了),但如果代码不是自己写的,那可真要命了。
分析上述例子,变化点是按钮的需求变更,所以需要对按钮类进行抽象化,提取一个抽象按钮类,界面类再根据抽象按钮类进行编程。
目前Java没有涉及,书中写到:在Java语言中,可以通过配置文件、DOM解析技术和反射机制将具体类类名存储在配置文件中,再在运行时生成其实例对象。我觉得这种方式很好,如果按钮类齐全,都可以不用重新编译代码,直接修改配置文件即可适应需求的变更。修改后的类图如下所示:里氏代换原则
里氏代换原则(Liskov Substitution Principle)定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都能代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
我目前的理解是:开闭原则的核心是进行抽象化,而里氏代换原则提供了抽象的方法,即在保证程序的行为没有变化的前提下,找到类可以代换的点,并将其抽象。
依赖倒转原则
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是实现面向对象设计的主要机制,依赖倒转原则是系统抽象化的具体实现。
依赖倒转原则(Dependence Inversion Principle)定义:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
另一种表述:要针对接口编程,不要针对实现编程。
直接举例说明:
某系统提供一个数据转化模块,可以将来自不同数据源的数据转化成多种格式,如可以转换来自数据库的数据,也可以转化来自文本文件的数据,转换后的格式可以是XML文件,也可以是XLS文件。
设计的原始类图如下:
可以看到这个设计违背了开闭原则,因为MainClass都依赖了具体的类,以下使用依赖倒转原则对其进行重构。
重构之后可以看到,高层模块依赖于抽象,实现了针对接口编程。
接口隔离原则
接口隔离原则(Interface Segregation Principle)定义:客户端不应该依赖那些它不需要的接口。另一种定义:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
这个原则我感觉和单一职责原则有些类似,需要根据经验划分和分割接口。
合成复用原则
合成复用原则(Composite Reuse Principle)定义:尽量使用对象组合,而不是继承来达到复用的目的。
继承和合成这两种复用机制的特点如下:
(1)通过继承来进行复用实现很简单,而且子类可以覆盖父类的方法,易于扩展。但主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是透明的,所以这种复用是透明的复用,又称“白盒”复用。如果基类发生改变,那么子类的实现也不得不发生改变。
(2)由于组合(或聚合)关系可以将已有的对象纳入新对象中,使之称为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现细节对于新对象是不可见的,所以这种复用又称为“黑盒”复用。
可见,相对继承关系而言,组合关系的耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
下面用例子理解一下继承和复用的设计:
上图中使用继承的方式来复用数据库的连接,但当需要更换数据库连接方式的时候,基类需要修改,子类也跟着需要修改,违背开闭原则,系统扩展性差。
将以上类图修改为:
如上图,原来的继承关系变成组合关系,并依赖于抽象类,而具体的连接方式通过继承来扩展。可见组合和复用要合理选择,这应该也是需要学习后续设计模式的一个原因——借鉴优秀的设计方法。
迪米特法则
迪米特法则(Law of Demeter)有多种定义方法,其中几种典型的定义如下:
(1)不要和“陌生人”说话。
(2)只与你的直接朋友通信。
(3)每一个软件单元对其他的单元都只有最少的知识,而且局限于那些与本单元密切相关的软件单元。
对比以下两张类图,感觉能了解到一些迪米特法则的理念:直观上感觉是联系关系更清楚了,应该也就更容易扩展维护。而且感觉这种方式也有点像网络上的路由机制,也有点像进程间通信机制,这样做便于流量控制、便于权限访问控制。