-
设计模式-七大软件设计原则
基本原则
开闭原则
在设计的时候尽可能的考虑,需求的变化,新需求来了尽可能少的改动代码,拥抱变化
定义:指的是软件中一个实体,如类、模块和函数应该对扩展开放,对修改关闭
。
面向抽象编程
开闭是对扩展和修改的约束
强调:用抽象构建框架,用实现扩展细节。
优点:提高软件系统的可复用性及可维护性
- 面向对象最基础的设计原则
-
指导我们构建稳定的系统
- 代码不是一次性的,更多时间在维护
- 大多是代码版本的更新迭代
-
我们最好对已有的源码很少修改
- 一般都是新增扩展,类来修改
- 能够降低风险
关于变化
-
逻辑变化
-
比如说算法从
a*b*c
变化成a*b+c
其实是可以直接修改的,前提是所有依赖或者关联类都按照相同的逻辑来处理
-
比如说算法从
-
子模块变化
- 子模块变化可能直接引起整体也就是高层的变化
-
可见视图变化
- 如果说需求上多了一些原有逻辑不存在的,可能这种变化是恐怖的,需要我们灵活的设计
例子
- 弹性工作时间,时间是固定的,上下班是可变的
顶层接口
接口是规范,抽象是实现
通过继承来解决
价格的含义已经变化了,所以不能够子类直接继承getPrice()
,因为当前已经是折扣价格了,可能需要价格和折扣价格
问题
为什么要遵循开闭原则,从软件工程角度怎么理解这点。
- 开闭原则对扩展开放对修改关闭,程序和需求一定是不断修改的,我们需要把共性和基础的东西抽出来,把常常修改的东西让他能够扩展出去,这样我们程序后期维护的风险就会小很多
为什么重要
-
对于测试的影响
- 一处变更可能导致原有测试用例都不管用了
-
提高复用性
- 高内聚,低耦合
- 提高可维护性
- 面向对象开发的要求
如何使用
- 抽象约束
-
参数抽到配置中
- 例如sql的连接信息
- 国际化信息
-
指定项目章程
- 约定项目中Bean都是用自动注入,通过注解来做装配
- 团队成员达成一致
- 公共类走统一的入口,大家都是用统一的公共类
- 封装变化
- 提前预知变化
依赖倒置原则
定义
高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。
说白了就是针对接口编程,不要针对实现编程
什么是倒置
- 不可分割的原子逻辑是底层模块,原子逻辑在组装就是高层模块
-
抽象就是接口或者抽象类
- 都不能被实例化的
-
细节
- 细节就是具体实现类
优点
- 通过依赖倒置,能够减少类和类之间的耦合性,提高系统的稳定性,提高代码的可读性和稳定性。降低修改程序的风险
例子
public class DipTest {
public static void main(String[] args) {
//===== V1 ========
// Tom tom = new Tom();
// tom.studyJavaCourse();
// tom.studyPythonCourse();
// tom.studyAICourse();
//===== V2 ========
// Tom tom = new Tom();
// tom.study(new JavaCourse());
// tom.study(new PythonCourse());
//===== V3 ========
// Tom tom = new Tom(new JavaCourse());
// tom.study();
//===== V4 ========
Tom tom = new Tom();
tom.setiCourse(new JavaCourse());
tom.study();
}
}
重点
- 先顶层后细节
- 自顶向下来思考全局不要一开始沉浸于细节
- 高层不依赖于低层,关系应该用抽象来维护
- 针对接口编程不要针对实现编程
以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此大家在拿到需求之后, 要面向接口编程,先顶层再细节来设计代码结构。
问题
为什么要依赖抽象,抽象表示我还可以扩展还没有具体实现,按照自己的话来解释一遍
- 一般软件中抽象分成两种,接口和抽象类,接口是规范,抽象是模板,我们通过抽象的方式,也就是使用规范和模板这样我们能够使得上层,也就是调用层能够复用逻辑,而我们底层是能够快速更改实现的,例如Spring的依赖注入,Dubbo的SPI,SpringBoot的SPI都如此
依赖的常见写法
- 构造传递依赖对象
- setter方法传递依赖对象
- 接口声明传递对象
最佳实践
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽 象才可能依赖倒置。
-
变量的表面类型尽量是接口或者是抽象类
- 很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对 化了,比如一个工具类,xxxUtils一般是不需要接口或是抽象类的。还 有,如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供 的一个规范。
-
任何类都不应该从具体类派生
- 如果一个项目处于开发状态,确实不应该有从具体类派生出子类的 情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在 所难免的,因此只要不超过两层的继承都是可以忍受的。特别是负责项 目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上 都是进行扩展开发,修复行为,通过一个继承关系,覆写一个方法就可 以修正一个很大的Bug,何必去继承最高的基类呢?(当然这种情况尽 量发生在不甚了解父类或者无法获得父类代码的情况下。)
-
尽量不要覆写基类的方法
- 如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要 覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一 定的影响。
单一职责原则
不要存在多余一个导致类变更的原因
- 类
- 接口
- 方法
只负责一项职责
如果不是这样设计,一个接口负责两个职责,一旦需求变更,修改其中一个职责的逻辑代码会导致另外一个职责的功能发生故障。
案例
用户信息案例
上述图片用户的属性和用户的行为并没有分开
-
下图把
- 用户信息抽成BO(Business Object,业务对象)
- 用户行为抽成Biz(Business Logic 业务逻辑对象)
电话
电话通话会发生下面四个过程
- 拨号
- 通话
- 回应
- 挂机
上图的接口做了两个事情
-
协议管理
- dial 拨号接通
- hangup 挂机
-
数据传送
- chat
引起变化的点
- 协议接通会引起会引起变化(连接导致不传输数据)
-
可以有不同的通话方式
打电话
,上网
从上面可以看到包含了两个职责,应该考虑拆分成两个接口
优点
- 类的复杂性降低,实现什么职责都有清晰明确的定义;
- 可读性提高,复杂性降低,那当然可读性提高了;
- 可维护性提高,可读性提高,那当然更容易维护了;
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责 做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
注意
单一职责原则提出了一个编写程序的标准,用“职责”或“变 化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都 是不可度量的,因项目而异,因环境而异。
This is sometimes hard to see
,单一职责确实收到很多因素制约
- 工期
- 成本
- 技术水平
- 硬件情况
- 网络情况
- 政府政策
接口隔离原则
- 两个类之间的依赖应该建立在最小的接口上
-
建立单一接口,
不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
高内聚低耦合
例子
问题
为什么要把IAnimal拆分成IFlyAnimal,ISwimAnimal,不拆分会有什么样的问题
- 一个类所提供的功能应该是他所真正具有的,不拆分会导致他不提供的功能但是强行需要实现,而且会有臃肿的类出现
- 可能适配器模式也是为了解决这个问题吧
最佳实践
- 一个接口只服务于一个子模块或者业务逻辑
- 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量 让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
-
已经被污染了的接口,尽量去修改,若变更的风险较大,则采用
适配器模式
进行转化处理; - 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!
迪米特法则
一个对象应该对其他对象保证最少的了解,也称最少知道原则
,如果两个类不必彼此直接通信,那么这两个类就不应该发生直接的相互作用,如果其中一个类需要调用另外一个类的某个方法的话,可以通过第三者转发这个调用
能够降低类与类之间的耦合
强调只和朋友交流
出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类, 而出现在方法体内部的类不属于朋友类。
这里面感觉有点职责分开的感觉,不同的对象应该关注不同的内容,所做的事情也应该是自己所关心的
例子
teamLeader只关心结果,不关心Course
错误类图如下
问题
如果以后你要写代码和重构代码你怎么分析怎么重构?
- 先分析相应代码的职责
- 把不同的对象需要关心的内容抽离出来
- 每个对象应该只创建和关心自己所关心的部分
- 一定要使用的话可以通过三方来使用
- 合适的使用作用域,不要暴露过多的公共方法和非静态的公共方法
注意
迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的 public方法和非静态的public变量,尽量内敛,多使用private、packageprivate、protected等访问权限。
在实际的项目中,需要适度地考虑这个原则,别为了套用原则而做项目。原则只是供参考,如果 违背了这个原则,项目也未必会失败,这就需要大家在采用原则时反复 度量,不遵循是不对的,严格执行就是“过犹不及”。
序列化引起的坑
-
谨慎使用Serializable
- 在一个项目中使用 RMI(Remote Method Invocation,远程方法调用)方式传递一个 VO(Value Object,值对象),这个对象就必须实现Serializable接口 (仅仅是一个标志性接口,不需要实现具体的方法),也就是把需要网 络传输的对象进行序列化,否则就会出现NotSerializableException异 常。突然有一天,客户端的VO修改了一个属性的访问权限,从private 变更为public,访问权限扩大了,如果服务器上没有做出相应的变更, 就会报序列化失败,就这么简单。但是这个问题的产生应该属于项目管 理范畴,一个类或接口在客户端已经变更了,而服务器端却没有同步更 新,难道不是项目管理的失职吗?
遵循的原则
如果 一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响, 那就放置在本类中。
里氏替换原则
一个软件实体如果能够适用一个父亲的话,那么一定适用其子类,所有引用父亲的地方必须能透明的使用其子类的对象,子类能够替换父类对象
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
-
子类的方法重载父类的方法时,入参要比父类的方法输入参数更
宽松
-
子类实现父类方法的时候(重写/重载或实现抽象方法),方法的后置条件(方法的输出,返回)要比父类更加
严格或者相等
例子
价格重写问题
价格不是直接重写,而是新写一个方法
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return super.getPrice() * 0.61;
}
}
长方形和正方形问题
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
public class Square extends Rectangle {
private long length;
//胜率
@Override
public void setHeight(long height) {
setLength(height);
}
}
当前设计会出现死循环
解决办法
抽象接口
public interface QuadRangle {
long getWidth();
long getHeight();
}
返回共同的length
public class Square implements QuadRangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getWidth() {
return length;
}
public long getHeight() {
return length;
}
}
当前方式子类就能够随时替换父类了
问题
- 你怎么理解里氏替换原则,为什么要保证使用父类的地方可以透明地使用子类
- 子类必须实现父类中没有实现的方法
- is-a的问题
- 如果父类的地方替换成子类不行的话程序复杂性增加,继承反而带来了程序的复杂度
- 子类只能在父类的基础上增加新的方法
- 在具体场景中怎么保证使用父类的地方可以透明地使用子类
- 父类返回多使用具体实现,入参多使用抽象或者说顶层接口
- 子类可以新增一些自己特有的方法
注意
如果子类不能完整地实现父类的方法,或者父类的某些方法 在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚 集、组合等关系代替继承。
尽量避免子类的“个性”,一旦子 类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父 类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务 来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标 准。
合成复用原则
尽可能使用对象组合 has-a组合 或者是 contains-a聚合而不是通过继承来达到软件复用的目的。
-
继承是白箱复用
- 所有细节都暴露给了子类
-
组合和聚合是黑箱复用
- 对象歪的对象获取不到细节
优点
问题
为什么要多用组合和聚合少用继承
- 继承是侵入性的
- Java只支持单继承
- 降低了代码的灵活性,子类多了很多约束
- 增强了耦合性,父类修改的时候需要考虑子类的修改
- 会导致关键代码被修改
总结
如果你只有一把铁锤, 那么任何东西看上去都像是钉子。
- 适当的场景使用适当的设计原则
- 需要考虑,人力,成本,时间,质量,不要刻意追求完美
- 需要多思考才能用好工具