面向对象设计原则

583 阅读7分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 80 篇原创文章

1. 单一职责

1.1 如何理解单一职责

首先,单一职责的“一” 不是指1个职责,而是指一类职责,例如Java后端调用通常会分为三个类:

面向对象设计01.png

再如String工具类,Date工具类,File工具类,上面提到的这些类每一个都承担了一类职责,在每一个类中都允许有多个方法。

对应到生活中,警察,医生,教师都承担了一类职责,但他们可以做很多具体的事情。

因此在设计类时,只要做到包含一组内聚的,可以归属到某一分类上的方法则可以认为是做到了单一职责。

1.2 单一职责的好处

假设你是一个开发,让你同时做开发、测试,你能都做好么?有的人可能会说,我是全栈,啥都可以搞定,那么再让你同时做UI原型设计,JAVA、C++、GO开发,手机端,云端自动化以及性能测试,你还能搞定不?或者哪个公司有高级的全栈工程师?

所以,一个人不可能搞定所有事情,只有在某一个方向专一,如开发,架构,测试,项目管理,才有可能达到顶尖级别,而对于类职责划分也类似,需要保证职责单一。

当类职责单一时,

● 首先,改动的可能性小,对应体现为稳定;

● 其次,阅读起来容易理解,对应体现为易维护(一本书既讲财务,又讲政治,不太可能都讲好);

● 再者,调用方数量减少,对应体现为耦合减少(试想一下,一个人既做医生,又做警察,和他打交道的人是不是必然比他只有一个职责的时候多)

所以单一职责的好处是:稳定、易维护、解耦。

1.3 如何做到单一职责

做到单一职责需要考虑两点:内聚和单一。

内聚指的是:把一组相近的功能放到一起,不要为了单一职责硬把各个方法拆开,放到不同类中。

举个例子:某系统/模块对外提供功能时,经常会用到一个设计模式是Facade,这个模式会将外部需要调用的一些方法,都集中封装到一个类中,方便外部找到调用入口,而不是按照功能拆得很细,不看文档都不知道方法入口在哪。

前面提到的controller,service,DAO类也一样,每个类都不会只有一个方法,所以,单一职责不是一定要把类拆得很细,只要做到内聚则可。

单一职责在划分类职责的时候可以从以下几方面考虑:

● 非业务的通用部分抽象并独立出来,例如:写文件,压缩,加解密,访问数据库,发消息,业务规则算法(算税,算优惠)等等。

● 按照业务域来划分,例如:客户,产品,订单。

● 按照分层架构来划分,例如前面提到的Controller,Service,DAO。

不管是上述哪一个方面在划分类职责时都可以参考几个原则:

● 某个功能是否能独立存在和变化,只要能,就可以拆开。例如客户的住址和单位信息可以独立存在和变化,因此可以拆出来,再如订单和订单项,彼此依赖,则需要放到一起管理。

● 业务语义是否可独立。只要业务语义可独立,就可分离出来,这个在下一篇中会举一个具体的例子说明。

● 内聚,上面提到的Facade模式就是一个内聚的例子,其内聚的考量是方便外部同一调用。

1.4 单一职责总结

● 单一职责的好处是:稳定、易维护、解耦,它们能大大提高生产力,每个开发都应该努力做到它。

● 归根结底,要做到单一职责,就是在划分类职责时做到内聚和将可独立存在和变化的部分拆分出来。

2. 开闭原则

2.1 定义

对扩展开放,对修改封闭。可以理解为尽量对已有类/模块的修改封闭,可以增加新的类实现扩展。

2.2 例子

算税根据不同的场景计算方法不同,需要动态获取算法,同时允许定制团队扩展税的算法。

● 这里税的算法是变化点,因此需要抽象出来。

● 定制团队要能扩展税的算法,因此需要支持动态注册算法。

最终得到的类结构图如下:

面向对象设计02.png

这样当有新的税算法时:

● 实现新的税算法类

● 增加一个新的税类型和新算法类对应

● 通过TaxCalculatorRegistry类注册新的税算法类

● 通过TaxCalculatorRegistry获取对应的税算法类算税

3. 里氏替换

3.1 定义

子类对象能够替换父类对象,并且保证原来程序的逻辑行为不变及正确性不被破坏。

这个定义看起来很像面向接口编程,但实际两者不同,里氏替换原则满足面向接口编程,但是面向接口编程未必满足里氏替换原则,里氏替换原则还包含了约束,强调程序的逻辑行为不变及正确性不被破坏。

3.2 例子

一个接口的子类原来性能差,替换了新的子类实现,接口不变,但是某个方法原来未抛异常,新的子类抛了运行时异常,这就改变了原来的逻辑行为,会影响调用端的处理逻辑,严格讲不符合里氏替换原则。

当代码被第三方调用时尤其要注意里氏替换原则,如果未告知抛异常情况下突然抛了异常,很有可能严重影响调用端的处理逻辑。

4. 接口隔离

4.1 定义

接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

4.2 例子

鸭子会叫,会游泳,会飞,但并非所有的鸭子都会游泳,会飞(例如塑料鸭),因此需要做接口隔离分别定义游泳和飞的接口。

如果不做接口隔离,那么塑料鸭子就要实现不必要的接口,但实际又不能有对应实现,只能抛出UnSupportException异常,有点多此一举,没有意义。

4.3 例外

在实际工作中,有时会违背这样的原则,将所有的接口定义在一起,在不支持子类实现中抛出UnSupportException,这种做法其实违背了接口隔离原则,但有时也是不得已为知,因为接口不是自己定义的,是开源框架或者业界标准接口定义,比如JDBC相关的接口,在和公司自行实现的数据库对接时,未必能支持所有方法,但又不得不通过JDBC接口做标准化。

因此,此原则应尽量遵守,也可以有例外。

5. 依赖倒置

5.1 定义

高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。

5.2 例子

通常情况下,高层会依赖低层,但这样就没有了扩展性,会依赖低层的实现细节,依赖倒置通常出现在业界的一些标准规范,框架等等,例如JDBC接口规范:\

面向对象设计03.png

调用者只需要依赖JDBC接口,而不用关心各个厂家的具体实现类。

类似的例子还有servlet规范,开发者只要实现servlet的接口就能在对应的web容器中被调用。

依赖倒置原则减少了低层耦合,适用于框架,模块间,系统间的调用。

6. 迪米特法则

6.1 定义

迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。英文简写为: LOD

6.2 例子

设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。

end.


<--阅过留痕,左边点赞!