面向对象的设计原则
大纲
- Single Responsibility Principle -- 单一职责原则(SRP)
- Open Closed Principle -- 开闭原则(OCP)
- Liskov Substitution Principle -- 里氏替换原则(LSP)
- Law of Demeter -- 迪米特法则(LoD)
- Interface Segregation Principle -- 接口隔离原则(ISP)
- Dependence Inversion Principle -- 依赖倒置原则(DIP)
- Composite Reuse Principle -- 合成复用原则(CRP)
单一职责原则
There should never be more than one reason for a class to change.
一个类应该只有一个发生变化的原因
在系统设计的过程中,无论是面向过程的,还是面向对象的语言,我们都会尝试将我们庞大的系统拆分成相对独立的模块,然后在基于这些模块进行编排,最终形成系统.
当这种非常基础的设计理念,具化到Java这类面向对象的语言时,则会要求我们设计类在功能上尽可能的独立,同时职责上尽可能做到最小的粒度.单一职责原则就是对这种设计理念的核心概述.
优点:
- 单个类的复杂度降低
- 降低代码阅读难度
- 降低代码维护的复杂度
缺点: - 实际设计的过程中,同一个抽象实体,在不同角度的职责拆分结果也是不同的.
- 会出现大量职责相似的类,对这些类的编排工作可能会导致维护复杂度升高.
小结
单一职责原则到底要实现到怎样的程度,取决于系统的实际情况,很多时候,避免过度设计是软件工程中非常重要的一环.
开闭原则
Software entities like classes, modules and functions should be open for extension but closed for modification
软件实体,如类、模块和函数应该对扩展开放,但要对修改关闭
开闭原则是面向对象开发中非常基础的原则,相较于其他几种原则,开闭原则更加的基础.工程师在设计系统的时候,一方面要考虑如何将显示世界中的实体,行为,业务抽象成程序语言中的类,一方面还要考虑系统开发过程中代码的开发,迭代,维护等问题,开闭原则主要是针对(开发,迭代,维护等)这方面问题如何解决的思考总结.
当我们设计一个类(或模块)的时候,当它的功能经过测试后,任何新的需求的跟进,都不要在现有的功能上做修改,而应该通过扩展的方式进行处理,这句话理解起来并不是非常的复杂,但如果一些base的部分没有处理好,开闭原则将会造成灾难性的后果.
- 实体的设计(结构+功能)上必须从最简单的开始.
- 善用抽象类和接口.
- 越是靠近业务出口端,代码也应该越简单.
小结
必须强调的是,开闭原则是非常底层的设计思想,同时也需要和其他的设计原则结合在一起使用才能够体现出设计优势.
里氏替换原则
Inheritance should ensure that any property proved about supertype objects also holds for subtype objects
继承必须确保超类所拥有的性质在子类中仍然成立
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
从现实角度上讲,LSP要求父类中已经实现的方法,在其子类中不可以被修改(复写),这其实是开闭原则在继承机制下的一个重要实现,但需要强调的是,由于面向对象的语言,允许抽象类(和接口)的存在,所以开闭原则并不等价于LSP.
LSP的优点在于:
- 避免了子类和父类行为不一致的问题
- 更低的维护复杂度和更良好的扩展性
- 避免由于子类复写父类方法导致的功能缺失问题
正方形不是长方形问题
这个例子充分展示了LSP原则面临的问题
class Rectangle {
Double width;
Double height;
Rectangle(Double width,Double height){
this.width = width;
this.height = height;
}
Double getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
Square(Double width, Double height) {
super(width, height);
checkSquare();
}
void checkSquare() {
super.height = super.width;
}
}
public class TestCase {
public static void main(String[] args) {
Rectangle rectangle = new Square(4.0, 5.0);
System.out.println("area-->"+rectangle.getArea());
}
}
area-->16
我们知道正方形是特殊的长方形,所以将正方形声明为长方形的子类,并在长方形中创建了面积计算公式(长✖️宽),在正方形中扩展声明了checkSquare()方法,将正方形设置成长=宽,并根据LSP不修改父类(长方形)计算面积的方法.最终导致了面积计算的错误.
类似的还有鸵鸟不是鸟的问题,为了完善LSP,需要做出一些限定:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相
小结
当然,里氏替换原则(LSP)并不能解决所有的设计问题,且对如何设计抽象类的子类描述也很模糊,反倒是为完善LSP所指定的那些细则更加的有实用价值,例如子类应该如何编写一个重载的父类方法.
迪米特法则
Talk only to your immediate friends and not to strangers
只与你的直接朋友交谈,不跟“陌生人”说话
迪米特法则又被称作最少知识原则(Least Knowledge Principle,LKP),主要是针对系统设计中实体与实体之间而言的.如果两个实体之间无需直接通信,那么就不应该产生直接调用,你可以通过第三者来建立两个实体的关系,这种设计原则的主要目的是解耦,提高模块相互之间的独立性.
众多的设计模式都参考了这种设计思想,它的有点也是显而易见的:
- 减少了实体类所涉及的"宽度"和"深度"
- 解耦
- 高解耦带来的高复用性和高扩展能力
同时,正如很多设计模式提到的额缺点(这是高解耦的通病):
- 大量的中介类提高了阅读成本
- 系统的复杂性提升
- 结构混乱
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
- 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中
P.S.对于5,不同的序列化会导致中介类在协调实体类的过程中出现属性解析异常的问题.
总结
权衡迪米特法则规范了面向对象设计上应该如何解耦,正如所有设计原则所面临问题的本质一样,高内聚与低耦合本身是相互冲突的,迪米特法则也要避免过犹不及的情况出现.
接口隔离原则
- Clients should not be forced to depend on methods they do not use
客户端不应该被迫依赖于它不使用的方法- The dependency of one class to another one should depend on the smallest possible interface
一个类对另一个类的依赖应该建立在最小的接口上
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
P.S.理解上面两句话的重点在于,两者的思考层面有所不同,例如你实现了一个类(包括其内部方法),单一职责原则(SRP)会建议你尽可能的使这个类这些功能实现上向"高内聚"特性靠拢,这其中当然包括了功能角度和业务角度的考虑.接口隔离原则(ISP)并非是在这个层面上的考虑,当你在设计系统架构的时候,必定会涉及到一些接口的继承,这是抽象式编程必要且重要的环节,ISP的目的是当你需要通过接口(甚至抽象类)来完善你的项目时,ISP建议这些接口满足高内聚,低耦合的设计理念(即上面1,2的要求).
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点:
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
在具体应用接口隔离原则时,应该根据以下几个规则来衡量:
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
小结
除了老生常谈的"要避免过犹不及"问题,还有一点就是接口隔离原则(ISP)有时候会对代码理解造成干扰,这种干扰产生的原因往往不是ISP的问题,而是我们在设计接口的时候没能区分好业务类型的接口和功能类型的接口,此时需要配合分层思想设计你的系统.
依赖倒置原则
High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
首先,依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
依赖倒置原则的主要作用如下:
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
小结
关于依赖倒置原则(DIP),重点在于如何调整以往的编程习惯,习惯抽象式编码,这对于系统架构的优化是明显的,需要工程师在系统前期架构的时候格外注意.
合成复用原则
when the software is reused, it is necessary to use association relationships such as combination or aggregation to achieve it first, and then consider using inheritance relationships to achieve it.
当你的软件需要被复用时,有必要优先考虑使用组合或聚合的方式实现关联,其次再考虑使用继承的方式实现
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
小结
需要注意的是,合成复用原则(CRP)对接口和抽象类的适用性,抽象编程同样适用于合成复用原则,但这种使用并不是在强调抽象类(或接口)必须要有子类继承实现的行为违反了原则(CRP),这是在不同层面的讨论,同时也侧面的强调了单一的设计原则并不是最好的,一定要根据你的项目实际情况做出权衡.
结论
本文由 momoker 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jun 21,2023