读《设计模式之美》笔记系列,以及对于其他一些已阅的内容简单总结
Summary of 7 principles 太长不看版
| 设计原则 | 归纳 | 目的 |
|---|---|---|
| 开闭原则 OCP | 对扩展开放,对修改封闭 | 降低修改带来的风险 |
| 里氏替换 LSP | 不要破坏继承体系子类应该可以替换父类,但子类修改不应该影响父类方法含义 | 防止继承泛滥 |
| 依赖倒置 DIP | 高层不应该依赖底层,面向接口编程 | 避免底层依赖带来的变动风险 |
| 单一职责 SRP | 只做一件事,类的单一 | 有助于提高代码可读性和可维护性 |
| 接口隔离 ISP | 一个类对另外一个类的依赖应该建立在一个最小的接口上 | 功能解耦,高聚合、低耦合 |
| 最小知识 LKP | 如无必要,尽量不要直接引用 | 减少代码臃肿 |
| 合成复用 CRP | 尽可能使用组合,而非通过继承 | 降低代码耦合 |
其中, 单一职责(SRP), 开闭原则(OCP), 里氏替换(LSP), 接口隔离(ISP), 依赖倒置(DIP) 共同构成了大名鼎鼎的 SOLID 原则。
单一职责原则 Single Responsibility Principle, SRP
-
key point:
- 一个类被引起变化的原因应该有且仅有一个,否则这个类应该被拆分为多个类
- 一个对象如果承担了太多职责,会给其他代码带来非必要的其他功能的代码,带来代码上的引入冗余;一个职责的改变或许会牵连这个对象其他功能职责的内容修改
-
benefit:
- 每个类只负责一个职能会简化这个类的功能复杂度
- 提高类的可读性以及可维护性
- 变更所引起的修改风险也会减低,当修改某个功能时,可以显著降低这个功能修改带来的其他影响
-
How:
- 适当进行抽象建模,将相关性高的内容聚合在一块
-
简单的判定依据:
-
类的代码行数过多,或者是包含的属性、方法过多,可以考虑将其拆分
-
依赖或者被依赖的其他类过多的时候,可以考虑将其拆分
-
比较难给类起一个合适的名字,那可能这个类的职责不够清晰
-
使用类的大量方法都是几种操作某个及属性值
-
开闭原则 Open Closed Principle, OCP
key point:
- 软件实体对扩展开放,对修改关闭 (Software entities should be open for extension,but closed for modification) 对拓展开放是为了应对变化,对修改关闭是防止影响以往的代码和单元测试
- 当需求修改时,应该扩展该模块的功能,以最小修改代价来满足新的需求;
- 只要没有破坏原来的代码的正常运行、没有破坏原有的单元测试,则可以认为是符合开闭原则的
benefit:when 遵循开闭原则
- 测试时只需要对拓展的代码进行测试,不影响原有代码
- 颗粒度越小,被复用的概率越高,可以提高代码的复用性
- 提交软件的可维护性,易于扩展和维护
How:
- “抽象约束,封装变化”,通过接口或者抽象类为实体定义一个相对稳定的抽象层
- 将可变的部分封装起来,隔离变换,提供抽象方法、接口给上层,当实际发生变化的时候,只需要扩展一个新的实现、并替换,尽可能避免上游代码的改动(约定好返回的内容、数据格式;调用方法和入参列表,减少改动)
- 预先留好扩展点,以便未来需求变更时,以最小代码改动代价,来将代码灵活插入到新的扩展点上
具体方法 OCP 的方法
- 多态
- 依赖注入
- 基于接口而非实现编程
- 抽象意识
里氏替换原则 Liskov Substitution Principle, LSP
-
key point: When we need inheritance
- 确保超类所拥有的性质在子类中依然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)
- 主要说明了什么时候该用继承,什么时候不行,里氏替换是对开闭原则的补充,反映子类和基类之间的作用
- 原则是可以用子类去替换掉所使用的的父类,同时保证不改变原有程序逻辑以原有程序的正确性。按照协议来设计,遵循父类的对外承诺的协议来设计子类
-
benefit:
- 保证类的拓展不会对已有的系统引入新的错误,保证兼容性前提下对父类进行拓展和调整
-
How:
- 子类可以拓展父类,但是不能改变父类原有的功能,子类所继承父类原有的功能逻辑、输入输出以及异常约定都要保持与原来一致。
- 子类不能去覆写父类已经实现的具体方法,防止引起其他子类的混乱
- 用父类的单元测试去检测子类是否可以通过,如果不能通过,则说明子类未能很好遵从父类的约定,那么子类可能违反了 LSP
-
违反 LSP 的例子
- 子类继承了父类的排序方法,父类是按照时间排序,而子类是按照 id 排序,这则违反了原来父类的设计
- 父类某个函数约定中,约定入参可以为任何数;而在子类中修改为只允许正整数,入参的约定父子类存在不一致,则违反了原来父类的设计
接口隔离原则 Interface Segregation Principle, ISP
-
key point:
- 为各个类提供它们专供的接口,而非试图建立一个庞大的接口去供给所有依赖它的类进行调用,一个类对另外一个类的依赖应该建立在一个最小的接口上
- 尽可能将庞大的接口拆分成更小或者更加具体的接口
-
Different with SRP 单一职责 两者均为提高内聚性,降低类之间的耦合性而提出,但二者各有不同
单一职责 接口隔离 侧重点 更加注重职责单一 注重对于接口依赖的隔离 约束对象 约束类的实现和细节 约束接口,针对抽象和程序框架的构建 -
benefit:
- 将接口的颗粒度拆分得更小,可以使得系统更加灵活,防止外来变更扩散修改风险
- 提高系统的内聚性,减少对外交互,降低系统的耦合性
- 接口颗粒度大小必须适度,太小会使得接口数量过多,导致设计复杂;太大则灵活度降低,无法进行定制化服务,提高系统风险
- 可以减少系统接口无用代码
-
How:
- 一个接口应该只服务于一个子模块或者一个业务逻辑
- 只提供给调用方所需要的接口方法
- 拆分颗粒度时,要注重了解业务逻辑,不同的业务环境或者业务逻辑所采用的接口拆分标准或许也会不同
- 提高内聚,减少对外交互,最大程度利用好接口,去完成更多的事情
-
Example:
- reference: blog.csdn.net/zhengzhb/ar…
依赖反转 Dependence Inversion Principle, DIP
-
key point:
-
高层模块不应该依赖低层模块,两者都应该依赖于同一个抽象;抽象不应该依赖细节,细节应该依赖抽象 (High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions. )
- 在调用链上,调用者属于上层,被调用者属于底层。这条原则实际上更多是指导框架,e.g. Tomcat, Tomcat 属于上层,WebApp 属于下层,两者都应该依赖于同一个抽象,Sevlet 规范,这个规范不依赖于具体 Tomcat 或者 WebApp 的实现细节,而 Tomcat 或者 WebApp 都应该依赖于 Sevlet 规范。
-
只定义好接口或者抽象类去规范,而非具体实现细节,具体的细节交由具体实现类去进行实现
-
-
benefit:
- 降低类间的耦合性,提高系统的稳定性
- 减少并行开发引发风险,提高代码可读性和和维护性
-
How:
- 每个类尽量提供接口或者抽象类
- 任何类都不应该从具体类派生
- 任何继承都要遵守 LSP
-
Compare with 控制反转 IOC、依赖反转 DIP、依赖注入 DI
-
控制反转 IOC - demo/pattern/principle/IOC
- 控制 指的是程序员对于程序执行的控制权,通过某种方式,使得程序的控制权转让给了框架或者其他,而非我们程序员撰写对应的实现过程,由此实现了 IOC
- IOC 不是一种具体的实现技巧,而是一个设计思想,用以指导框架层面的设计
-
依赖注入 DI
-
注入 指的是不通过 new() 的方式在类内进行创建,而是在类外实例化所需要依赖的对象或者实例之后,通过构造函数或者函数的方式**传入(注入)**给类里面进行使用。
-
这样的方式有利于提高类的拓展性,不需要改动类内的实现,通过外部传入即可,外部的对象只要满足一定接口约束,可以进行其他的拓展。
-
DI 是一种具体的编码技巧
- with IOC: 类对象的创建和依赖注入工作和本身具体业务无关,可以抽象出来让框架自己来完成,这类框架就是依赖注入框架。简单配置类对象、类与类之间的关系,剩下由框架自己完成创建、管理对象生命周期、以及依赖注入等,例如: Spring 框架主要是通过依赖注入来实现控制反转。
-
-
依赖反转 DIP
-
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象 (High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions. )
- 在调用链上,调用者属于上层,被调用者属于底层。这条原则实际上更多是指导框架,e.g. Tomcat, Tomcat 属于上层,WebApp 属于下层,两者都应该依赖于同一个抽象,Sevlet 规范,这个规范不依赖于具体 Tomcat 或者 WebApp 的实现细节,而 Tomcat 或者 WebApp 都应该依赖于 Sevlet 规范。
-
-
-
依赖注入 & 面向接口编程的区别?
- 依赖注入是一种具体的编码技巧,所依赖的其他类,在类外实例化后传入,而非自己在类内进行实例化。强调对象创建和类之间的关系,可以进行灵活替换,提高代码扩展性。
- 面向接口编程指的是约定一种接口规范,屏蔽掉其他的差异,依赖于接口,而非具体实现的类。强调上下游的稳定性,降低耦合度,提高扩展,关注抽象和实现。
KISS & YAGNI & DRY
Kiss - 如何做,尽量保持简单
- Keep it Simple & Short
- Keep it Short & Simple
- Keep it Simple & straightforward
YAGNI - 要不要做,不要过度设计
- You ain't gonna need it
- 预留合理的拓展点,但是不要提前设计编码本不需要的代码
DRY - 重复代码
-
Don’t repeat yourself.
-
典型的情况
- 实现逻辑重复: 是否违反 DRY,要根据实际场景来分析,避免违反了单一职责和接口隔离。同时可以考虑在更小维度下进行拆分,拆分成细粒度的导函数来解决。e.g. 检查 username 和检查 password,虽然可能内容是一样的,但是本质是校验不同的内容。
- 功能语义重复:如果最后达成的目标是一致的,那么就是违反了 DRY。e.g. 有两个不同方式实现用来检验 ip 地址是否合法,这个地方两个方法虽实现方式,但是最后都是用来校验 ip 地址,那么就是违反了 DRY。
- 代码执行重复: 可能在一个串行执行的程序中,对相同的内容检验了两次,导致代码执行重复。这个时候可以考虑将其抽到顶层作为入参校验,避免两次重复校验。
-
如何提高代码复用性
- 减少耦合,避免牵一发而动全身的模块代码。
- 满足单一职责,避免大而全。
- 模块化
- 业务和非业务的代码逻辑进行分离。
- 通用代码下沉,杜绝下层代码调用上层代码。从分层的角度而言,越是底层的代码越是具有通用性。
- 封装、抽象、继承、多态
- 应用模板模式等设计模式
-
When?
- 过早重构反而会有错误抽象的风险,当发生变更时,反而引入更多错误。Attempting premature refactoring risks selecting a wrong abstraction, which can result in worse code as new requirements emerge[2] and will eventually need to be refactored again.
- Rules of Three: 当一个方法重复了2次,即将重复第3次的时候,可以考虑将其重构。但是注意,这个 3 并非严格意义的3,而是当有复用需要的时候,即可以考虑对其进行重构。link: en.wikipedia.org/wiki/Rule_o…
迪米特法则, 最小知识法则 Least Knowledge Principle, LKP
-
key point:
- 减低类间的耦合度,提高模块相对的独立性。实现高内聚,松耦合。
- 目标是提高代码可读性和可维护性,缩小功能改动导致的代码改动范围。
- Each unit should ONLY talk to its friends, DON’T talk to strangers. 不该有直接依赖的类,不要直接依赖;有依赖关系的类,尽量只依赖必要的接口。
-
benefit: 援引子空间聚类目标,类内高度聚集,类间松散连接
- 松耦合:知道类与类之间的依赖关系,降低类间的耦合度,提高模块相对独立性
- 高内聚:指导类内设计,将相近的内容聚合到类内,提高类的可复用性和系统拓展性
-
How: 从依赖者的角度来说,只依赖应该依赖的对象;从被依赖者的角度说,只暴露应该暴露的方法。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
合成复用法则 Composite Reuse Principle, CRP
-
key point:
- 尽量先使用组合或者聚合的关联方式来实现,之后再考虑继承关系来实现
- 要使用继承关系,要严格遵循 里式替换原则 LSP 来实现
-
different with 复用类型:
- 合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
继承复用 组合复用 封装性 继承会将父类内容暴露给子类,父类对于子类来说是透明的,因此称之为“白箱复用” 维持了类的封装性,成分对象内部对新对象而言不可见,所以又称之为“黑箱复用” 耦合度 父类的任何变更,都会导致子类发生变更,不利于类的拓展和维护 新旧类耦合性低,新对象存取成分对象的方式就是通过成分对象的接口 灵活性 父类继承而来的实现是静态的,在编译时已经确定,所以在运行时不可能发生变化 可以在运行时动态变化,新对象可以动态地引用与成分对象类型相同的对象
For more information: