来自极客时间 王争设计模式之美 专栏总结
1. 如何判断是否需要 解耦
- 将模块与模块之间、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构 如果依赖关系复杂、混乱,那么代码的可读性和可维护性肯定不好
- 通过间接的衡量标准来辅助判断,看修改代码会不会牵一发而动全身
2. 如何给代码 解耦
- 封装和抽象作为两个非常通用的设计思想,可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。比如Unix系统的open()文件操作函数,通过将函数的复杂性封装在局部代码中。除此之外,因为 open() 函数基于抽象而非具体的实现来定义,所以我们在改动 open() 函数的底层实现的时候,并不需要改动依赖它的上层代码
- 引入中间层 引入中间层能简化模块或类之间的依赖关系同时引入中间层能够帮助我们重构,起到过渡的作用,能够让开发和重构同步进行。主要分为四个阶段
- 引入一个中间层,包裹老的接口,提供新的接口定义
- 新开发的代码依赖中间层提供的新接口
- 将依赖老接口的代码改为调用新接口
- 确保所有的代码都调用新接口之后,删除掉老的接口
- 模块化(本质 分而治之) 将系统划分成各个独立的模块,让不同的人负责不同的模块,这样即便在不了解全部细节的情况下,也能协调各个模块,让整个系统有效运转。同时每个模块只提供封装了内部实现的细节的接口给其他模块使用,这样可以减少不同模块之间的耦合度。
- 其他设计思想和原则
- 单一职责原则。高内聚会让代码更加松耦合,而实现高内聚的重要指导原则就是单一职责原则。模块或者类的职责设计得单一,而不是大而全,那依赖它的类和它依赖的类就会比较少,代码耦合也就相应的降低了
- 基于接口而非实现编程。基于接口而非实现编程能够通过接口这样一个中间层,隔离变化和具体的实现。这样做的好处是,在有依赖关系的两个模块或类之间,一个模块或类的改动,不会影响到另一个模块或类。固定了传入参数和返回值->基于最小接口而非最大实现编程
- 依赖注入。通过依赖注入的方式可以让耦合关系没有那么紧密,容易做到插拔替换。在外部创建的对象通过构造方法等的方式传入到类中进行可插拔式的使用。
- 多用组合少用继承。对于继承结构比较复杂的代码,利用组合来替换继承,组合主要通过 接口 来实现 接口表示 has-a 的方法 比如 会飞 会下单 都可以定义为接口通过组合的方式来实现一个类
- 迪米特法则。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口
- 接口隔离原则。一组 API 接口集合 | 单个 API 接口或函数 | OOP 中的接口概念
- 提高代码复用性
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
1. 基于接口而非实现编程
- 从本质上来看,”接口”是一组”协议”或者”约定”,是功能提供者提供给使用者的一个”功能列表”。
- 这个原则可以将接口和实现分离,封装不稳定的实现,暴露稳定的接口。上游系统使用接口时无需考虑接口的具体实现只需要考虑其具备当前的功能并且保证了实现的稳定性,从而使得调用者不用依赖不稳定的实现细节。
- 越抽象,越顶层,越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。
- 接口中 函数的命名不能暴露任何实现细节 比如 uploadToAliyun -> upload
- 封装具体的实现细节。比如,跟阿里云相关的特殊上传流程不应该暴露给调用者
- 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。
- 也可以先实现,然后将相应的方法搬到对应的接口设计中去,但是在搬移到接口定义中的时候,要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中。
2. 多用组合少用继承
- 在继承实现会非常复杂的时候,继承关系越来越深 无法掌控时。需要考虑使用组合、接口、委托三个技术手段来一块儿解决继承存在的问题。
- 接口表示具有某种行为特性。比如 “会飞” 作为一种行为特性,可以定义一个 Flyable 接口 然后让会飞的鸟类去实现接口。
- 如果接口只声明方法,不定义实现时,也就是每一个实现当前接口的类都要复写一遍方法,会导致代码重复的问题。这个时候可以利用委托+组合的方式来解决,写一个第三方的类来实现这个通用的方法,然后通过组合的方式在类中进行创建对象从而进行复用
- 组合 接口 委托 -> 继承的主要三个作用 表示 is-a 关系,支持多态特性,代码复用->组合+接口的 has-a 关系来替换 is-a 关系;利用接口来替换多态特性;组合+委托的方式来替换代码复用->通过 组合 接口 委托 的方式 我们完全可以替换继承
- 在实际的开发过程中,如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承
3. 单一职责原则(A class or module should have a single responsibility)
- 一个类只负责完成一个职责或者功能。也就是说,不要设计大而全地类,要设计粒度小,功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类
- 先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个我们就是可以将这个粗粒度的类,拆分成几个更细粒度的类。持续重构->先用一个 User 类 包含了 地址 邮箱 手机 等基础信息->为了支持电商业务则拆分地址信息 为了支持多个app同时登录则拆分邮箱 手机信息
- 指导使用单一原则的方法
- 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,可以考虑对类进行拆分
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合 高内聚 低耦合 的设计思想,我们应该考虑对类进行拆分
- 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用
- 比较难给类起一个合适名字,很难用一个业务名称概括,或者只能用一些笼统的 Manager Context 之类的词语来命名,这就说明类的职责定义不够清晰
- 类中大量的方法都是集中操作类中的某几个属性 比如 在 User 中如果一半的方法都是在操作 address 信息,就可以考虑将这几个属性和对应的方法拆分出来
- 当一个类的代码,读起来让你头大,实现某个功能时不知道该用那个函数,想用那个函数翻半天都找不到,只用到一个小功能就要引入整个类(类中包含很多无关此功能实现的函数),这就说明类的行数、函数、属性过多了
- 不要拆分过于细致,实际上可能会适得其反,反倒会降低内举行,也会影响代码的可维护性
- 内聚和耦合其实是对一个意思(即合在一起)从相反方向的两种阐述
1. 内核是从功能相关来谈,主张高内聚。把功能高度相关的内容不必要地分离开,就降低了内聚性,成了低内聚
2. 耦合是从功能无关来谈,主张低耦合。把功能明显无关的内容随意结合起来,就增加了耦合性,成了高耦合。
4. 对扩展开放 对修改关闭
- 利用 多态、依赖注入、基于接口而非实现编程
// 这一部分体现了抽象意识 public interface MessageQueue { //... } public class KafkaMessageQueue implements MessageQueue { //... } public class RocketMQMessageQueue implements MessageQueue {//...} public interface MessageFromatter { //... } public class JsonMessageFromatter implements MessageFromatter {//...} public class Demo { private MessageQueue msgQueue; // 基于接口而非实现编程 public Demo(MessageQueue msgQueue) { // 依赖注入 this.msgQueue = msgQueue; } // msgFormatter:多态、依赖注入 public void sendNotification(Notification notification, MessageFormatter msgFormatter) { //... } }
5. 接口隔离原则
- 把”接口”理解为一组 API 接口集合。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者调用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不被用到的接口
- 把 “接口” 理解为单个 API 接口或函数
- 在统计数据的 最大值 最小值 总和时 可以设计两类接口 每个都单独拆分 一类是全部统计同时返回->在不同的业务场景中会对应的不同的使用场景也即都是合理的
- 判断接口是否职责单一的标准: 通过调用者如何使用接口来间接判断-> 如果调用者只使用部分接口或者接口的部分功能,那接口的设计就不够职责单一
- 把 “接口” 理解为 OOP 中的接口概念->也即 接口 组合 委托 来实现
6. 依赖注入原则
- 不通过 new() 的方式在类的内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递给类使用
- 通过依赖注入 + 多态的方式即可实现方法调用的可插拔的方式
7. 高内聚 低耦合
- 高内聚就是相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
- 低耦合就是说在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。依赖注入、接口隔离、基于接口而非实现编程->实现代码的低耦合
- 在松耦合部分可以考虑的一个点就是 只依赖有限接口方法 不需要大而全的接口方法->基于最小接口而非最大实现编程