写点什么

一文带你读懂:设计模式的六大原则

发布于: 2021 年 01 月 24 日
一文带你读懂:设计模式的六大原则

1、写在开头

因此在学习设计模式之前,我们先回顾六大原则,它们分别是:

  • 单一职责原则(Single Responsibility Principle)

  • 开闭原则(Open Closed Principle)

  • 里氏替换原则(Liskov Substitution Principle)

  • 迪米特法则(Law of Demeter),又叫“最少知道法则”

  • 接口隔离原则(Interface Segregation Principle)

  • 依赖倒置原则(Dependence Inversion Principle)

2、单一职责原则(Single Responsibility Principle)


单一职责原则内涵:

单一职责原则是针对类来说的,即一个类应该只负责一项职责。


例子 1:      如类 T 负责两个不同职责:职责 P1,职责 P2。当职责 P1 需求变更而改变 T 时,可能造成职责 P2 发生故障,所以需要将类 T 的粒度分解为 T1,T2。


例如,产品经理就是整理需求,程序员就是写代码,分工明确:


//产品经理public class ProduceManager {  //...整理需求}//勤奋的程序员public class Coder { //...写代码}
复制代码


大白话:

一个类干好自己应该干的事情,别越界而导致业务耦合。


好处:

  1. 类的复杂性降低,实现什么职责都有清晰明确的定义;

  2. 可读性高,复杂性降低,可读性自然就提高了;

  3. 可维护性提高,可读性提高了,那自然更容易维护了;

  4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

3、开闭原则(Open Closed Principle)

开闭原则内涵:

一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。


例子 2:

我们的公司里面有许许多多的程序员,无论男女都在认真的写代码。但是有天我们的程序媛们觉得自己有“加工资的需求”。此时,我们可以将这个动作抽象到新的接口里面,然后 FemaleCoder 去实现这个动作以完成拓展。



//公司工作 class Company{ private List<Coder> coders = new ArrayList<>(); void working(){ for (Coder code : coders){ code.coding(); } } } //程序员写代码interface Coder{ void coding();}//程序媛class FemaleCoder implements Coder{ @Override public void coding() { //... }}//程序猿class MaleCoder implements Coder{ @Override public void coding() { //... }}
复制代码


新的接口标识“涨工资的需求”:



//待遇福利interface Salary{ //涨工资动作 void raise();}
复制代码


修改新增的 FemaleCoder 类:



//程序媛class FemaleCoder implements Coder, Salary{ @Override public void coding() { //... }
@Override public void raise() { //...我要加薪 }}
复制代码


4、里氏替换原则(Liskov Substitution Principle)

里氏替换原则内涵:

所有引用基类的地方必须能透明地使用其子类的对象。

遵循里氏替换原则,在子类中尽量不要重写和重载父类的方法。


举例子 3:

我们的子类程序猿 (MaleCoder) 和程序媛(FemaleCoder) ,同继承了基类程序员(CoderPeople)。

那么凡是程序员使用的地方,无论对于程序员还是程序媛,都应该可以无缝替换。



//写代码的人class CoderPeople { public void coding(){ //... }}//程序媛class FemaleCoder extends CoderPeople{ @Override public void coding() { //... }}//程序猿class MaleCoder extends CoderPeople{}
复制代码


调用点:



public class LiskovSubstitutionPrinciple { public static void main(String[] args) { //用 CoderPeople 的子类 FemaleCoder 的实例来替换 CoderPeople的实例,程序工作正常 working(new FemaleCoder()); } private static void working(CoderPeople shape){ System.out.println("开始写代码"); shape.coding(); System.out.println("结束写代码"); }}
复制代码


UML:



大白话:

  • 里氏替换原则首先突出的是面向对象的三大特性之一:“继承”。它要求我们去遵守,是因为不遵守导致的代价太大了。

  • 使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。


好处:

大大降低程序运行出错的概率。

5、接口隔离原则(Interface Segregation Principle)

接口隔离原则内涵:

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。(原则中的接口,是一个泛泛而言的接口,不仅仅指 Java 中的接口,还包括其中的抽象类)


    举例子 4:

    这个图的意思是:类 A 依赖接口 I 中的方法 1,方法 2,方法 3,类 B 是对类 A 依赖的实现;类 C 依赖接口 I 中的方法 1,方法 4,方法 5,类 D 是对类 C 依赖的实现。对于类 B 和类 D 来说,虽然存在用不到的方法(红色标记所示),但由于实现了接口 I,所以也必须要实现这些用不到的方法。



如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。

如果将这个设计修改为符合接口隔离原则,就必须对接口 I 进行拆分。在这里我们将原有的接口 I 拆分为三个接口,拆分后的设计如图所示:



注意:

  1. 可能会觉得接口隔离原则和之前的单一职责原则很相似,其实不然。

  2. 单一职责注重职责,而接口隔离原则注重对接口依赖的隔离;

  3. 单一职责是约束类,其次是方法,针对的是程序中的实现和细节;而接口隔离原则约束的是接口,针对的是抽象,程序整体框架的构建。


大白话:

  • 定义接口时要权衡考虑是否可拆分多个模块中。

  • 使用接口时要权衡考虑是否简化实现的接口列表。


好处:

  • 使用多个专门的接口。专门的接口也就是提供给多个模块的接口。

  • 提供给几个模块就应该有几个接口,而不是建立一个臃肿庞大的接口,所有的模块都可以访问。

6、迪米特法则(Law of Demeter)

迪米特法则内涵:

一个对象应该对其他对象有最少的了解。

这个例子中,我们知道 总公司经理 管理  总公司员工,分公司经理 管理 分公司员工。

下面的例子是符合迪米特法则的:



//总公司员工class Employee{ private String id;}//分公司员工class SubEmployee{ private String id;}//分公司经理class SubCompanyManager{ List<SubEmployee> subEmployees = new ArrayList<SubEmployee>();}//总公司经理class CompanyManager{ List<Employee> employeeList = new ArrayList<Employee>();}
复制代码


下面的例子是不符合迪米特法则的,因为分公司员工对于总公司经理来说,是不需要知道的类:



//总公司经理class CompanyManager{ //总公司员工 List<Employee> employeeList = new ArrayList<Employee>(); //分公司员工 List<SubEmployee> subEmployees = new ArrayList<SubEmployee>();}
复制代码


UML:



大白话:

一个类对自己需要耦合或者调用的类知道的最少,你类内部怎么复杂,我不管,那是你的事,我只知道你有提供好的公用方法,我能正常调用即可。


7、依赖倒置原则(Dependence Inversion Principle)

依赖倒置原则内涵:

高层模块不应该依赖低层模块,二者都应该依赖其抽象;

抽象不应该依赖细节,细节应该依赖抽象。

核心思想是面向接口编程。


举例子 6:

程序员喜欢搬砖,随着年龄和经验增加,从一开始搬的普通砖头,逐渐变为银砖,到后来慢慢变为金砖...

那么分析可以知道:程序员是高层模块,而“普通砖头”、“银砖”和“金砖”都视为底层模块。


底层模块

//砖头interface Brick{}//石头砖头class StoneBrick implements Brick{}//银砖头class SilverBrick implements Brick{}//金砖头class GoldBrick implements Brick{}
复制代码


高层模块



//搬砖的程序员class CoderMan{ void movingBrick(Brick brick){ //.... }}
复制代码


如此一来,不管哪种砖头,程序员都可以搬了。(无论以后怎样扩展 Brick 类,都不需要再修改 CoderMan 类了)


    CoderMan coderMan = new CoderMan();        coderMan.movingBrick(new StoneBrick());       coderMan.movingBrick(new SilverBrick());       coderMan.movingBrick(new GoldBrick());
复制代码


UML:



大白话:

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。


好处:

  • 实际情况中,代表高层模块的 CoderMan 类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。

  • 所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

8、总结

文章主要参考书籍有《大话设计模式》和网上一些零散的文章,写出来的目的一方面是自己对这六项原则系统地整理一下,另外也因为设计模式对编程人员来说的确非常重要,笔试面试都会用得到,希望对大家有所帮助。


9、延伸阅读

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式


《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭


《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》


《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

《读懂排序算法(六):二分查找算法》


《设计模式》

设计模式之六大设计原则

设计模式之创建型(1):单例模式

设计模式之创建型(2):工厂方法模式

设计模式之创建型(3):原型模式

设计模式之创建型(4):建造者模式


发布于: 2021 年 01 月 24 日阅读数: 32
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
一文带你读懂:设计模式的六大原则