一、开闭原则
定义:一个软件实体如类、模块函数应该对扩展开放,对修改关闭。强调的是用抽象构建框架,用实现扩展细节。以提高软件系统的可复用性及可维护性帮助我们实现稳定灵活的系统架构。生活中的实例(弹性工作制,每天必须工作满八小时这个是不能修改的,但是对于什么时候来什么时候走没有规定)。实现开闭原则的核心思想是面向抽象编程。
代码场景:web应用通常是有过滤器的,通常有很多个,每个都是一类功能的集合,一般我们只是通过继承 Filter 新增实现类的方式去添加新的过滤器来丰富我们的功能,而不是在原来的过滤器上做修改,将新的功能添加进去。这种就是对扩展开放,对修改关闭。
举例:小明同学
class XiaoMing{
public String name;
public int age;
public String sex;
}
小明同学唱歌,错误的写法
class XiaoMing{
public String name;
public int age;
public String sex;
public void sing(){
// 唱歌
}
}
根据开闭原则,用拓展的方式:
class XiaoMingLearn extends XiaoMing {
public sing() {
// 唱歌
}
}
二、依赖倒置原则
定义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。
代码场景:
汽车A类拥有run方法
class CarA {
public run () {
// 我跑起来了
}
}
司机-小瓜拥有drive汽车A的方法
class XiaoGua {
public driveA (car: CarA) {
car.run()
// 司机小瓜驾驶汽车A
}
}
这时候多了一辆汽车B类,也拥有run方法
class CarA {
public run () {
// 我跑起来了
}
}
司机小瓜想驾驶汽车B类,就需要添加一个新的drive方法,因为此时汽车A类跟汽车B类不是同类型
class XiaoGua {
public driveA (car: CarA) {
car.run()
// 司机小瓜驾驶汽车A
}
public driveB (car: CarB) {
car.run()
// 司机小瓜驾驶汽车B
}
}
这时我们发现,模块之间的依赖太严重,根据依赖倒置原则,可以这样实现
// 先创建一个drive接口
interface IDrive {
drive: (car: Car) => {
// 驾驶技巧
}
}
// 小瓜通过IDrive接口去实现驾驶技巧
class XiaoGua implements IDrive {
drive!: (car: Car) => {
car.run()
// 小瓜学会了驾驶技巧
};
}
// 再给汽车创建一个通用类,且都可以驾驶
class Car {
public run () {
// 可驾驶
}
}
class A extends Car {
public name = 'A车'
}
class B extends Car {
public name = 'B车'
}
// 小瓜驾驶A类车或者B类车
class Test {
public driveCar () {
const xiaogua = new XiaoGua()
xiaogua.drive(new A())
xiaogua.drive(new B())
}
}
三、单一职责
定义:不要存在多于一个导致类变更的原因。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
一个职责的变化可能会影响、削弱或者抑制这个类实现其他职责的能力;
当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费;
代码场景:一个类/接口/方法只负责一项职责
四、接口隔离原则
定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
- 一个类对应一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 适度原则,一定要适度
代码场景:
小铭同学会唱歌会跳舞还会打篮球
class XiaoMing {
public sing() {}
public dancing() {}
public playBasketball() {}
}
根据接口隔离原则,可以这样写,把唱歌跳舞打篮球分成不同模块
interface ISingAction {
sing: () => {}
}
interface IDancingAction {
dancing: () => {}
}
interface IPlayBasketballAction {
playBasketball: () => {}
}
当小铭需要作出某种行为的时候
class XiaoMing implements ISingAction {
public sing: () => {}
}
class XiaoMing implements IDancingAction {
public dancing: () => {}
}
class XiaoMing implements IPlayBasketballAction {
public playBasketball: () => {}
}
五、迪米特原则(最少知道原则)
定义:一个对象对其他对象保持最少的了解,尽量降低类与类之间的耦合,强调只与相关类交流。相关类指的是出现在成员变量、方法的输入、输出参数中的类。
代码场景:
校长需要老师统计班上学生的年龄,校长不需要关心学生的类型,由老师来统计
class Student {
public age!: number;
}
class Teacher {
public countAge() {
const list = [];
for (let i = 0;i < 20; i++) {
list.push(new Student().age)
}
}
}
class Principal {
public commandCountAge(teacher: Teacher) {
teacher.countAge()
}
}
六、里氏替换原则
定义:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
代码场景:最著名的就是“正方形不是长方形”
七、合成复用原则
定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
1.继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
2.子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
3.它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
1.它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
2.新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
3.复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。