面向对象设计原则和设计模式关系及如何实践

988 阅读8分钟

banner窄.png

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

相关阅读:

JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则


1. 概述

设计模式,面向对象原则,软件质量属性的关系如下图:

  1. 上层所作的一切都是为了最终能提高软件质量属性。

  2. 设计原则基于面向对象三大原则,这三大原则是上层的基础。

  3. 封装变化是面向接口编程的基础,如果没有变化就没有必要面向接口编程,因此封装变化尤为重要。

  4. 设计模式遵循了设计原则和面向对象三大原则,同时又是这些原则的落地实践参考。

以上各层互相关联,需要综合来看才能更好的落地实践,提高个人能力和软件质量。

2. 设计原则

这里并不会对设计原则做详细介绍,这方面的资料较多,可以参考其他资料一起着看,这里仅描述基本概念和一些体会。

2.2. 单一职责

参考Java编程思想(八)如何做到单一职责

2.3. 开闭原则

2.3.1. 定义

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

2.3.2. 例子

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

  1. 这里税的算法是变化点,因此需要抽象出来。
  2. 定制团队要能扩展税的算法,因此需要支持动态注册算法。

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

这样当有新的税算法时:

  1. 实现新的税算法类
  2. 增加一个新的税类型和新算法类对应
  3. 通过TaxCalculatorRegistry类注册新的税算法类
  4. 通过TaxCalculatorRegistry获取对应的税算法类算税

2.4. 里氏替换

2.4.1. 定义

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

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

2.4.2. 例子

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

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

2.5. 接口隔离

2.5.1. 定义

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

2.5.2. 例子

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

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

2.5.4. 例外

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

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

2.6. 依赖倒置

2.6.1. 定义

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

2.6.2. 例子

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

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

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

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

2.7. 迪米特法则

2.7.1. 定义

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

2.7.2. 例子

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

3. 面向对象三大原则

3.1. 封装变化

封装变化的好处显而易见,向上支持设计原则,向下保证了软件的多个质量属性。 那么怎么才能提高封装变化的能力?

通常可以在设计阶段先做分析,初步识别哪些功能可能是变化点,画出大致的类结构图,开始编码,但实际上设计阶段有的细节考虑不到,另外也并不是每个人都会设计先行,因此还可以通过逆向的方式识别变化点,过程如下:

  1. 编写代码时每个方法尽量不要超过20行,当你这么做以后,每个可以独立存在的方法都可能是一个变化点。(方法短小不仅仅可以用来识别变化点,同时也能提高可维护性,好比一篇文章,为啥要有章节,没有章节目录的书如果内容又长,估计你都不想看,所以编写短小方法应该始终贯彻到底)
  2. 检查每个方法的实现是否可能发生变化,是否可提取成公共方法,如果答案为肯定,就意味着你要提供接口,拆分类,使用工厂,策略等设计模式
  3. 进一步检查这个变化点是否允许第三方来扩展,如果答案为肯定,意味着你可能要提供动态注册变化点的实现类的能力,或者是对外提供的方法允许通过接口注入具体的实现类。
  4. 进一步检查变化点是否需要独立为一个子包,或者是项目级的公共包 按照以上方式步骤不断重构就可以了。

注意事项:

  1. 每个可独立的方法不一定就要做成public,要根据实际需要决定是否要做成public,方法独立最基本的作用是提高可维护性。
  2. 要充分考虑变化的可能性,如果可能性不大,就不要作为变化点处理,不要为了让代码看起来很高大尚就搞出接口,设计模式来,代码简单,稳定更重要。

参考:
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(五)事件通知模式解耦过程
Google guava源码之EventBus

3.2. 面向接口编程

参考:
JAVA编程思想(二)如何面向接口编程
JAVA基础(五)函数式接口-复用,解耦之利刃

3.3. 优先使用组合而非继承

参考:
Java编程思想(七)使用组合和继承的场景

4. 落地实践总结

1. 设计模式,设计原则,面向对象三大原则互相关联,需要结合起来实践。

2. 面向对象的三大原则是基础,其中封装变化点又是基础的基础,要优先提高这个能力,在实践中编写小方法,并识别是否可能的变化点,有助于提高这个能力。

3. 当识别到变化点以后,对类职责怎么划分,是否要独立拆分类,分包;是否要使用面向接口编程,可以套用哪个设计模式就好决策了。

4. 在不断重构中优化,不要一上来就非得套个设计模式,面向接口编程,只要能合理拆分方法,从而划分类职责,又有相关的知识背景,重构起来很快。

end.


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