文章目录
- 面向对象设计原则
- 1. 开闭原则(OCP - Open Close Principle)
- 2. 依赖倒转原则(DIP - Dependence Inversion Principle)
- 3. 里氏替换原则(LSP - Liskov Substitution Principle)
- 4. 单一职责原则(SRP - Single Responsibility Principle)
- 5. 合成复用原则(CRP - Composite Reuse Principle)
- 6. 迪米特法则(LOD - Law Of Demeter)
- 7. 接口隔离原则(ISP - Interface Segregation Principle)
- 总结
面向对象设计原则
在软件设计时,为了提高软件的可扩展性、可复用性、可维护性和灵活性,开发人员要尽量遵守面向对象的设计
原则,从而提升开发效率、节约开发成本和维护成本,同时面向对象设计原则也是学习设计模式的基础
有人说面向对象设计原则共有 6 个,也有人说共有 7 个,具体的数量我们不去追究,因为它们的核心思想都是一
样的,所以本文本着多多益善的原则,将记录面向对象的七个原则
个人建议:面向对象设计原则虽好,但不要在项目中滥用,否则会适得其反,只有那些预测未来经常会变、会经常
扩展的地方,才建议考虑参照面向对象设计原则
1. 开闭原则(OCP - Open Close Principle)
开闭原则是,对新增功能开放,对修改原有代码逻辑关闭
举例:线上运行的项目中已有一个微信支付功能,其对应的模块类是 Pay,该类中内容非常多,逻辑非常复杂,现
在产品要求再新增一个支付宝支付功能,那么我们就不应该在 Pay 中进行修改,而是应该再创建一个新的类,这就
是一个非常简单的满足开闭原则的设计
在设计类似功能时,设计者应该具备提前预判该功能未来会有多种实现方式的能力,在设计初期就以满足开闭原则
的结构来设计功能模块
2. 依赖倒转原则(DIP - Dependence Inversion Principle)
依赖倒转原则是,要关联、组合、聚合、依赖抽象类或者接口,而不是具体实现类
举例:现有电脑类,西部数据硬盘类,因特尔 CPU 类,为了实现未来扩展性,我们不应该让电脑类直接组合西部数
据类和因特尔 CPU 类,而是需要对西部数据类和因特尔 CPU 类进行抽象,提取成接口或抽象类,然后让电脑类
组合抽象类或接口,这样更利于扩展
3. 里氏替换原则(LSP - Liskov Substitution Principle)
里氏替换原则是,子类可以扩展父类的方法,但不要重写父类的方法
如果不遵守里氏替换原则,当子类重写父类方法后,如果重写的内容完全偏离父类的原来意图,可能会出现
意想不到的错误
举一个比较呆的例子:
- 设计端:创建一个类 NewList 继承 ArrayList 并将 add 方法重写成移除操作
- 使用者:调用了 NewList 的 add 方法,按照常识,本意想做添加操作
- 运行:结果却是移除操作
4. 单一职责原则(SRP - Single Responsibility Principle)
单一职责原则是,一个类应该只专注一个业务模块,类中只提供该模块相关的功能
举例:一个类只专注一个领域,如果一个类中有查询用户、新增用户、删除用户、新增部门、删除部门等功能,
那这个类就违反了单一职责,应该考虑重构该类为一个用户类,一个部门类
5. 合成复用原则(CRP - Composite Reuse Principle)
合成复用原则是,尽量避免继承,而是使用聚合和组合的方式来复用其他类,继承的方式实现复用耦合性太强,而
且也不够灵活
举例来一个不算很好的例子,将就着领会一下意思:
6. 迪米特法则(LOD - Law Of Demeter)
迪米特法则是,类中不应该依赖与其不发生直接关系的类,如果要依赖,应该有个中间类
举例,现在要实现男孩用电脑打游戏,得先缕清关系,男孩用电脑是直接关系,电脑打游戏是直接关系,男孩和游
戏是间接关系,所以男孩类中不应该依赖游戏类,而电脑类恰好适合当作中间类,因为它与二者都是直接关系
这个例子如果只看 UML 图,可能不太好理解,现在写出代码:
不满足迪米特法则,因为依赖了间接关系的类:
/**
* 游戏类
*/
class Game {
/**
* 游戏名
*/
public String gameName;
}
/**
* 电脑类
*/
class Computer {
/**
* 用电脑玩游戏
*
* @param game 依赖电脑类
*/
public String play(Game game) {
return game.gameName;
}
}
/**
* 男孩类
*/
class Boy {
/**
* 男孩名
*/
public String boyName;
/**
* 用电脑玩游戏
*
* @param computer 依赖电脑对象
* @param game 依赖游戏对象
*/
public void play(Computer computer, Game game) {
System.out.println(boyName + " 玩 " + computer.play(game));
}
}
满足迪米特法则,因为所有的依赖,都是直接关系:
/**
* 游戏类
*/
class Game {
/**
* 游戏名
*/
public String gameName;
}
/**
* 男孩类
*/
class Boy {
/**
* 男孩名
*/
public String boyName;
}
/**
* 电脑类
*/
class Computer {
/**
* 玩游戏
*
* @param boy 依赖男孩对象
* @param game 依赖游戏对象
*/
public void play(Boy boy, Game game) {
System.out.println(boy.boyName + " 玩 " + game.gameName);
}
}
7. 接口隔离原则(ISP - Interface Segregation Principle)
接口隔离原则是,接口功能要尽可能少,但是也要有限度
接口如果不提供方法的默认实现的话,实现类就必须得实现那些抽象方法,这就导致有一些实现类并不需要使用
那么多的接口方法,但又却不得不实现
代码演示:
不满足接口隔离原则,因为 Person 不会飞,而又不得不实现接口中的 fly 方法
/**
* 动作接口,接口过于臃肿,复用性不好
*/
interface Action {
/**
* 飞
*/
public void fly();
/**
* 走
*/
public void walk();
}
/**
* 人类
*/
class Person implements Action {
/**
* 飞,人不会飞,但是因为实现了 Action 接口,所以必须实现该方法
* 违法了接口隔离原则
*/
@Override
public void fly() {
}
/**
* 走
*/
@Override
public void walk() {
}
}
/**
* 鸟类
*/
class Bird implements Action {
/**
* 飞
*/
@Override
public void fly() {
}
/**
* 走
*/
@Override
public void walk() {
}
}
满足接口隔离原则,每个类只实现其需要实现的接口方法
/**
* 飞的动作接口
*/
interface Fly {
/**
* 飞
*/
public void fly();
}
/**
* 走的动作接口
*/
interface Walk {
/**
* 走
*/
public void walk();
}
/**
* 人类, 只需要实现走的动作
*/
class Person implements Walk {
/**
* 走
*/
@Override
public void walk() {
}
}
/**
* 鸟类,需要实现走和飞两个动作
*/
class Bird implements Fly, Walk {
/**
* 飞
*/
@Override
public void fly() {
}
/**
* 走
*/
@Override
public void walk() {
}
}
总结
不管是面向对象设计原则,还是设计模式,我们要学会站在设计者和使用者两个角度来看问题,不然很难理解这些
知识,我们参考设计原则或设计模式想要达到的效果其实只有两个:
-
站在类库或框架开发者的角度:当要更新类库或框架时,尽量不修改类库或框架中原来的代码逻辑,也不要修改方法签名,
可能很多人都在用我们的类库或框架,一旦原有逻辑修改错误或方法签名改变,会导致所有使用者都受影响 -
站在使用者角度:不管类库或框架怎么更新,都不要影响使用者的原有代码,当使用者自己想变更需求时,
最好只更换一个实现类就能完成功能
刚开始按照网上的例子学习设计模式或面向对象设计原则时,总有疑惑,开闭原则相关的例子,实现扩展后,客户
端的代码却总得跟着改,这是为什么?
这是因为网上的例子都是站在设计者和使用者双重角度来列举的,客户端代码改变是为了演示使用者自己想变
更需求时,类库或框架恰好扩展了该功能,客户端只要更换实现类即可的效果,如果使用者不想变更需求,客
户端是不需要改变的