中国的先贤说过: 有道无术,术可求.有术无道,止于术. 术指的是技能、技术或方法,而道指的是原则、道德、智慧和理念。
西方古代的哲人也说过同样的话: 智慧之路从感性开始,却终极于理性.为什么要说设计原则呢, 因为设计模式通常需要遵循一些设计原则,在设计原则的基础之上衍生出了各种各样的设计模式。设计原则是设计要求,设计模式是设计方案,使用设计模式的代码则是具体的实现。
设计模式中主要有六大设计原则,简称为SOLID ,是由于各个原则的首字母简称合并的来(两个L算一个,solid 稳定的),六大设计原则分别如下:
1、单一职责原则(Single Responsibitity Principle)
2、开放封闭原则(Open Close Principle)
3、里氏替换原则(Liskov Substitution Principle)
4、接口分离原则(Interface Segregation Principle)
5、依赖倒置原则(Dependence Inversion Principle)
6、迪米特法则(Law Of Demter)
1) 单一职责原则
一个类应该只有一个引起它变化的原因。换言之,一个类只负责一项职责。这样可以使得类更加可维护、可扩展、可重用。
在类的设计中 我们不要设计大而全的类,而是要设计粒度小、功能单一的类
生活中的例子
首先是单一职责原则: 一个类只负责一项职责, 比如说一辆汽车的刹车踏板,它的作用就是让行进间的汽车停止的, 如果现在这个刹车踏板的功能改成了,踩一半是油门, 踩到底是刹车,大家想一下 这会出现一种什么情况呢 ? 那现在世界上会开车的人 可能就非常少了, 估计女司机应该一个都没有. 这就是单一职责原则.
代码举例: 产品提出需, 要我们设计一个计算图形面积的类,如下:
接着又提出,要能够将计算结果以JSON格式打印出来,然后我们就添加了这样一个打印JSON格式的方法
请问在该类中添加打印方法是否合理 ?
答: 显然是不合理. 如果后面产品有提出了 打印XML格式...导出到Excel... 那
这个类就会变得十分臃肿,增加了很多本不属于他的责任
那该如何设计? 答: 将打印功能单独设计一个类出来
注意: 面试官问的是一个思路,看你是不是有这种设计思维 不会关心具体代码
单一职责原则的优点包括:
-
提高代码的可读性和可维护性:一个类只负责一个职责,代码更加清晰,更容易理解和维护。
-
降低类的复杂度:一个类只需要负责一个职责,类的复杂度更低,更容易进行测试和调试。
-
降低代码的耦合度:当一个类只负责一个职责时,不同的职责可以分配到不同的类中,不同的类之间相互独立,从而降低了代码的耦合度。
-
提高代码的可复用性:一个类只负责一个职责,可以更容易地被其他模块复用。
-
便于扩展和维护:当需求变化时,如果每个类只负责一个职责,我们只需要修改相关的类即可,而不需要修改其他的类,从而更容易进行扩展和维护。
延伸面试题: 如何判断一个类的职责是否单一?
类中的代码行数、函数、或者属性过多;
函数或者属性过多,说明该类设计的不够单一,很可能包含其他职责内容
类依赖的其他类过多
说明该类有一些本该属于自己的功能,被抽取到其他类中了
私有方法过多
过多的私有方法,说明该类很可能包含其他类的职责内容
类中大量的方法都是集中操作类中的几个属性
说明属性设计不合理,本应该设计在其他类的属性
总结: 不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的,最好的方式就是:
我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构
2) 开放封闭原则
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,我们应该通过添加新的代码来扩展软件的功能,而不是修改已有的代码。
生活中的例子
开放封闭原则: 对扩展开发,对修改关闭. 我还是用汽车举例, 我是一个广东人, 今年冬天我要开车去东北 去东北吃雪, 因为广东人好吃嘛,没吃过雪 想去长白山吃点新鲜的雪. 东北雪很大 就需要给这个车的轮胎做防滑. 其实很简单只要给轮胎装上防滑链就可以了,这就属于是对轮胎的开放扩充. 在轮胎上面新增了防滑链,没有改变轮胎的原有功能. 我们不能因为为了给轮胎做防滑,而把汽车引擎换掉了.
代码举例: 还是计算面积的功能,现在我们又有一个计算三角形面积的需求,应该怎么做呢?
方式1: 直接修改 AreaCalculator
上面的做法就违反了开闭原则,因为假设后面又增加了 计算正方形、圆形等等的需求的时候,就需要不断的修改该类的代码,增加新的函数.
想要满足开闭原则,就必须要使用顶层设计思维,来解决问题
顶层设计思维
抽象意识
封装意识
扩展意识
比如在这里我们先利用抽象思维,将各类图形进行抽取,设计一个接口来表示图形(抽象),然后利用扩展思维,在接口中有一个抽象的方法,该方法的功能是获取面积.
每增加一种图形,就去实现该接口,然后重写计算面积方法即可.
有了抽象之后,就可以利用抽象,修改计算面积的方法,将参数改为接口类型,该程序的扩展性 就提升了,再有新的图形添加,也不需要修改计算程序的代码,只需添加新的类即可,从而实现了开闭原则.
在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整 体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到"对扩 展开放、对修改关闭"。
开放封闭原则的优点包括:
-
提高代码的可维护性和可复用性:开放封闭原则要求我们使用扩展来增加功能,而不是直接修改原有代码,这样可以避免对原有代码的破坏,从而提高代码的可维护性和可复用性。
-
降低代码的耦合度:当需要增加新的功能时,我们可以通过扩展来实现,而不是修改原有代码,这样可以避免不必要的代码耦合,从而提高代码的灵活性和可扩展性。
-
提高代码的稳定性:当需要增加新的功能时,我们只需要扩展已有的代码,而不需要修改原有的代码,这样可以避免引入新的错误,从而提高代码的稳定性。
-
提高代码的可测试性:使用开放封闭原则可以避免对原有代码的破坏,从而更容易进行测试和调试。
-
提高代码的可维护性:使用开放封闭原则可以使得代码更加模块化,更容易进行维护和修改。
3) 接口隔离原则
客户端不应该依赖它不需要的接口。也就是说,我们应该将不同的接口拆分成更小的、更具体的接口,从而避免客户端依赖于它们不需要的方法。这样可以降低接口的复杂性,提高系统的可维护性和可扩展性。
生活中的例子
接口隔离原则: 客户端不应该依赖它不需要的接口。 我们拿汽车的方向盘举例, 我们通过转动方向盘,可以控制车辆的转向,对于我们开车的人来说只需要知道如何转动方向盘就可以了 , 例如 倒车入库: 1、左后视镜下沿与停止线重合,向右打死方向盘;2、看右后视镜,车身库角30cm到了,向左回一圈方向盘;3、当车门把手与库角重合,方向盘右打死;4、看左侧后视镜,看到后边库角露出10cm,方向盘回正。
用户操作方向盘并不需要知道车辆的引擎、刹车、变速器等部件的具体实现细节,它只需要提供一个简单的接口 就是方向盘,即让驾驶员可以方便地控制车辆的方向。
下面的代码,就是没有遵守接口隔离原则,其中Rectangle类 实现了不应该它实现的方法.
接口隔离原则的优点包括:
提高代码的灵活性和可维护性:接口隔离原则要求我们定义精简的接口,这样可以使得代码更加灵活和可维护,因为我们只需要实现我们真正需要的接口即可。
- 提高代码的可测试性:接口隔离原则可以使得代码更加容易进行测试和调试,因为我们可以只测试我们真正需要的接口。
降低模块之间的耦合度:当模块之间只依赖于真正需要的接口时,它们之间的耦合度更低,更容易进行组合和修改。
-
提高代码的可复用性:当接口精简清晰时,代码的可复用性也会提高,因为我们可以更容易地将代码组合到不同的应用场景中。
-
提高代码的安全性:接口隔离原则可以避免一些意外的依赖关系,从而提高代码的安全性。
延伸面试题: 接口隔离原则与单一职责原则的区别
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
4) 依赖倒置原则
高层模块不应该依赖于底层模块,而是应该依赖于抽象。也就是说,我们应该面向接口编程,而不是面向实现编程。
我们一起看一下下面代码有没有问题 ?
很显然上面的代码是有问题的, 我们应该遵守依赖倒置原则,高层模块不应该依赖于底层模块,而是应该依赖于抽象 .Car类是相对高层的,而QYEngine是底层的是具体实现.
依赖倒置原则的核心思想是:针对抽象编程,而不是针对具体实现编程。这样可以降低模块之间的耦合度,使系统更加灵活、可扩展和易于维护。同时,依赖倒置原则也可以促进面向对象设计的另一个原则------开闭原则的实现,即可以在不修改已有代码的情况下,通过添加新的实现来扩展系统的功能。
依赖倒置原则的优点包括:
-
提高代码的灵活性和可维护性:依赖倒置原则要求我们依赖于抽象而不是具体实现,这样可以使得代码更加灵活、可扩展和可维护。
-
降低模块之间的耦合度:当模块之间依赖于抽象而不是具体实现时,它们之间的耦合度更低,更容易进行组合和修改。
-
提高代码的可测试性:依赖倒置原则可以使得代码更加容易进行测试和调试,因为我们可以使用抽象来代替具体实现,从而更容易进行模拟和测试。
-
提高代码的可复用性:依赖倒置原则可以使得代码更加容易被复用,因为我们可以使用抽象来代替具体实现,从而更容易将代码组合到不同的应用场景中。
-
提高代码的可扩展性:依赖倒置原则可以使得代码更加容易进行扩展,因为我们可以通过添加新的实现类来扩展代码的功能,而不需要修改已有的代码。
延伸面试题: 关于依赖倒置原则、依赖注入、控制反转这三者之间的区别与联系
1 ) 依赖倒置原则
依赖倒置是一种通用的软件设计原则, 主要用来指导框架层面的设计。
高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
2 ) 控制反转
控制反转与依赖倒置有一些相似, 它也是一种框架设计常用的模式,但并不是具体的方法。
"控制"指的是对程序执行流程的控制,而"反转"指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员"反转"给了框架。
Spring框架,核心模块IoC容器,就是通过控制反转这一种思想进行设计的
3 ) 依赖注入
依赖注入是实现控制反转的一个手段,它是一种具体的编码技巧。
我们不通过 new 的方式在类内部创建依赖的对象,而是将依赖的对象在外部创建好之后,通过构造函数等 方式传递(或注入)进来, 给类来使用。
依赖注入真正实现了面向接口编程的愿景,可以很方便地替换同一接口的不同实现,而不会影响到依赖这个接口的客户端。
5) 里氏替换原则
子类必须能够替换掉它们的父类。也就是说,在任何使用父类的地方,都应该能够使用子类来替代,而且程序不应该出现任何错误或异常
下面是一个 Rectangle 类,它是用表示矩形的,它有一个获取面积的方法
接下来再设计一个 Square 类继承自 Rectangle 类表示正方形,该类的构造方法中只接收一个边长即可,但是要将继承子父类的长和宽属性 都进行填充.
并且 在 Square 类中,我们重写了父类 Rectangle 中的 setWidth 和setHeight 方法,这样可以确保 Square 的长和宽始终相等
在测试方法中,我们创建了一个 Square 对象并将其赋值给一个 Rectangle 类型的变量 squareAsRectangle 。最后,我们调用了 printArea 方法来打印矩形和正方形的面积,可以看到程序输出正确的结果。
这个例子展示了里式替换原则的一个基本思想:子类应该能够替换掉父类并且不会影响程序的正确性。在这个例子中,我们可以将 Square 对象视为Rectangle 对象来使用,因为它们都有相同的方法和属性。这就是里式替换原则的应用。
里氏替换原则的优点包括:
-
提高代码的灵活性:通过遵循里氏替换原则,我们可以在不影响程序正确性的情况下,更加灵活地使用不同的子类对象来实现不同的功能。
-
提高代码的可扩展性:通过遵循里氏替换原则,我们可以更容易地向系统中添加新的子类,从而实现代码的扩展。
-
提高代码的可维护性:通过遵循里氏替换原则,我们可以更容易地修改子类的实现,从而提高代码的可维护性。
-
提高代码的可读性:通过遵循里氏替换原则,我们可以使得代码更加符合人类的思维方式,从而提高代码的可读性。
-
降低代码的耦合度:通过遵循里氏替换原则,我们可以降低代码的耦合度,使得各个模块之间的关系更加清晰,更容易进行组合和修改。