面向对象设计原则
面向对象设计原则
- 目标:开闭原则
- 指导:最小知识原则
- 基础:单一职责原则、可变性封装原则
- 实现:依赖倒转原则、合成复用原则、里氏代换原则、接口隔离原则
单一职责原则SRP
Single Responsibility Principle
定义:一个对象应该只包含一个单一的职责,并且该职责被完整地封装在一个类中
也就是说,就一个类而言,应该仅有一个引起他变化的原因
- 数据职责:
- 通过类属性来体现
- 行为职责:
- 通过方法来体现
例子
重构后
开闭原则OCP
Open-Closed Principle
定义:一个软件实体应该对扩展开发,对修改关闭
也就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变模块的行为
抽象化
- 抽象化是开闭原则的关键
- 通过“对可变性封装”,找到系统的可变因素并将其封装起来
例子
用户可能要求使用不同的按钮
里氏代换原则LSP
Liskov Substitution Principle
定义1:对每一个类型为S的o1,都有类型为T的o2,使得以T定义的所有程序P在所有的对象o2都代换成o1时,程序P的行为没有发生变化,那么类型S是类型T的子类型
定义2:所有引用父类的地方都必须能透明地使用其子类的对象
(第一种定义相对严格)
通俗地说,在软件中如果能够使用父类对象,那么一定能够使用子类对象
分析
- LSP是实现OCP的重要方式之一
- 在程序中尽量用基类类型对对象进行定义,在运行时再确定其子类类型,用子类对象来替换父类对象
例子
运用LSP,改为
依赖倒置原则DIP
Dependence Inversion Principle
定义:高层模块不应该依赖底层模块,它们都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象
也就是说,要针对接口编程,不要针对实现编程
分析
- 如果说开闭原则是OO设计的主要目的,那么DIP就是OO设计的主要手段
- 实现方式之一是:在代码中使用抽象类,而将具体类放在配置文件中
- 三种耦合方式
- 零耦合
- 具体耦合
- 抽象耦合 √
例子
某系统提供数据转换模块,可以将来自不同数据源的数据转换成多种格式,如数据库、文本、XML等
由于需求的变化,可能需要增加新的数据源或者文件类型
接口隔离原则ISP
Interface Segregation Principle
定义:客户端不应该依赖那些它不需要的接口
注意:这里的接口指的是所定义的方法
也就是说,一旦一个接口太大,则需要把它分割成一些更细小的接口,使用该接口的客户端仅需要知道与之相关的方法即可
怎样定制接口
-
一个接口只代表一个角色
-
接口仅仅提供客户端需要的行为
- 隐藏客户端不需要的行为,应为客户端提供尽可能小的接口
-
必须满足单一职责原则
- 在高内聚的前提下,方法越少越好。但也不必过于极端,否则会接口爆炸
-
定制服务,为不同的客户端提供宽窄不同的接口
- 见下面的例子
例子
以下的系统定义了巨大的接口(胖接口)AbstractService
问题:
- ClientA可能会调用operatorB
- 假如ClientB修改operatorB,可能会影响到其他部分
修改:使用窄接口
合成复用原则CRP
Compose Reuse Principle
定义:尽量使用对象组合,而不是继承来达到复用的目的
组合和聚合的区别
- 聚合关系用一条带空心菱形箭头的直线表示,如下图表示A聚合到B上,或者说B由A组成;
- 与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在;例如, 部门撤销了,人员不会消失,他们依然存在;
- 组合关系用一条带实心菱形箭头直线表示,如下图表示A组成B,或者B由A组成;
- 但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了;例如, 公司不存在了,部门也将不存在了;
复用方法
- 继承复用(“白箱”复用)
- 实现简单,易于扩展
- 破坏系统封装性,暴露基类的实现细节
- 继承而来的实现是静态的,可以看作是一种代码的复制,不能在运行时发生改变
- 组合/聚合复用(“黑箱”复用)
- 耦合度低,选择性调用成员对象的操作
- 可以在运行时动态进行
例子
不好的实现方法:继承
问题:
- 如果需要修改数据库连接方式,需要修改DBUtil类源代码
- 如果StudentDAO和TeacherDAO要使用不同的连接方法,就需要新增一个新的DBUtil类,还要修改DAO使其继承新的DBUtil类
修改:使用组合
最小知识原则(迪米特法则)LoD
Low of Demeter
定义:
(1)不要和“陌生人”说话
(2)只和你的直接朋友通信
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位
简单来说,迪米特法则就是指一个单位实体应该尽可能少的与其他实体发生相互作用
其实叫最小知识原则就好,因为只是一个指导方针,而不是"必须按照",说法则太严格了
什么是直接朋友?
- 当前对象本身this
- 以参数形式传入到当前对象方法中的对象
- 当前对象的成员变量
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
- 当前对象所创建的对象
分析
狭义的迪米特法则
如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
如果一个类需要调用另外一个类的方法,可以通过第三者转发
以上图为例,如果A想调用C的方法:
错误的做法:A.B.C.method()
正确的做法:
A.method(){
B.wrapper();
}
B.wrapper(){
C.method();
}
- 好处:
- 降低类的耦合
- 坏处:
- 会在系统中增加大量的小方法
广义的迪米特法则
指对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制
- 类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 类的设计上,一个类型应当设计成不变类
- 对其它类的引用上,一个对象对其它对象的引用应当降到最低
例子
改进:
思考题
JDK中,java.util.Stack
是java.util.Vector
的子类,这样的设计合理吗?
- 违反了合成复用原则,应该使用组合
- 违反了里氏替换原则