物件导向设计的五原则 SOLID
Categories:
SOLID 原则对专案的影响很大,当专案一点一滴的导入 SOLID 原则的程式码,不少複杂的程式码慢慢被简化,被简化的程式码可以降低複杂度
,读懂程式码的时间从原本需要花 20 分钟阅读,到只需要花费 2 分钟阅读。
缩短阅读时间对专案来说是一件好事,一般来说工程师「阅读程式码」
的时间常常大于「新增/修改程式码」
的时间,毕竟要先读懂才能动手嘛,因此 缩短阅读程式码时间 等于 缩短「新增/修改程式码」 程式码的时间。
遵循 SOLID 原则的程式码具有以下特性
- 降低程式码複杂度
- 缩短阅读程式码的时间
- 减少维护专案程式码的时间
- 能够容忍变化
- 容易扩展新逻辑
- 容易理解
- 容易複用
SOLID 原则说明
S: 单一职责原则 (Single responsibility principle, SRP)
目的
每个物件,不管是类别、函数,负责的功能,都应该 只做一件事
。
原因
对函数而言,一个函数内,同时做了两件以上的事情。当发生错误时,很难快速定位错误的原因
。也容易间接导至程式码的可阅读性降低。
O: 开放封闭原则 (Open-Close principle, OCP)
目的
当需求有异动时,要如何在不变动现在正常运行的程式码,藉由继承
、相依性注入
等方式,增加新的程式码,以实作新的需求。
原因
假若为了新需求,去修改了原本的程式中的某一个函数,可能会造成其他呼叫使用该函数的的功能,出现非预期的错误。
L: 里氏替换原则 (Liskov substitution principle, LSP)
目的
子类别可以扩充套件父类别的功能,但 不改变
父类别原有的功能
原因
继承的特性导致高耦合,子类别对于方法修改(Override, Overload) 必须依照父类别行为方向
,否则会对整体的继承体系照成破坏,会有产生不可预测的行为
与不好察觉 Bug
假设你继承实作并且 override 的时候,必须先思考修改内容的行为方向是否符合父类别
,如果行为不符合就意味着其实一开始就不属于该父类别
范例
一般正常 继承(is-a)
,老鹰是鸟的一种,鸟为父类别,老鹰为子类别。而子类别也继承父类别的行为,比如说鸟会飞,老鹰为鸟的子类别,所以老鹰也会飞。
class Bird {
int flySpeed;
public void setFlySpeed(int flySpeed) {
this.flySpeed = flySpeed;
}
public int getFlyTime(int distance) {
return(distance/flySpeed);
}
}
class Eagle extends Bird {}
public class LSP {
public static void main(String[] args) {
Bird eagle=new Eagle();
eagle.setFlySpeed(120);
System.out.println("路程300公里:");
System.out.println("老鹰花了" + eagle.getFlyTime(300) + "小时.");
}
}
// 路程300公里:
// 老鹰花了2.5小时.
不会飞
的 企鹅
继承 鸟子类别
class Bird {
int flySpeed;
public void setFlySpeed(int flySpeed) {
this.flySpeed = flySpeed;
}
public int getFlyTime(int distance) {
return(distance/flySpeed);
}
}
class Eagle extends Bird {}
class Penguin extends Bird {
public void setFlySpeed(int flySpeed) {
this.flySpeed = 0;
}
}
public class LSP {
public static void main(String[] args) {
Bird eagle=new Eagle();
Bird penguin=new Penguin();
eagle.setFlySpeed(120);
penguin.setFlySpeed(20);
System.out.println("路程300公里:");
System.out.println("老鹰花了" + eagle.getFlyTime(300) + "小时");
System.out.println("企鹅花了" + penguin.getFlyTime(300) + "小时");
}
}
// 飞行300公里:
// 老鹰花了2.5小时
//
// Exception in thread "main" java.lang.ArithmeticException: / by zero
// at Bird.getFlyTime(LSP.java:7)
// at LSP.main(LSP.java:28)
没有办法计算出企鹅飞行的时间,因为企鹅根本不会飞,违反了 LSP 原则,所以 企鹅
根本不应该继承 鸟
,两者的属性不相同会导致发生意外的错误
Liskov Substitution Principle 的4个继承规范
- 子类别必须
完全实现
父类别的方法 - 子类别
可以有自己的特性
- 重载 (Overload) 或者实现父类别的方法时
输入参数可以被放大
- 复盖或者实现父类别的方法时
输出结果可以被缩小
LSP 的优缺点
优点
- 提高程式码的
重用性
- 提高类别的
扩充性
缺点
- 因为继承关係,
耦合性增高
(修改父类别常数时需要思考是否会影响到其他继承的子类别
) - 降低程式码灵活性 (必须实作父类别所有方法)
I: 接口隔离原则 (Interface segregation principle, ISP)
目的
针对不同需求的用户,开放其对应需求的介面,提拱使用。
原因
避免不相关的需求介面异动,造成被强迫一同面对异动的情况。
假设你有一个拥有10个函数的类别,现在有 3 个不同的客户端都需要使用这个类别其中的 4、7、5
个函数。
如果直接把这个类别传给三个不同的客户端,则这些客户端很可能会因为它所没使用到的函数改变了,也被迫跟着改变(因为原本类别的介面改变了)。
另一种作法,则是针对三个不同的客户端,提供三个不同的 Adapter
。透过 Adapter,只开放客户端所需的函数给它们,以隔离因为不相关介面改变所造成的客户端改变。
D: 依赖反转原则 (Dependency inversion principle, DIP)
目的
当 A 模组在内部使用 B 模组的情况下,我们称 A 为上层高阶模组 (caller)
,B 为底层低阶模组 (callee)
。
上层高阶模组 (caller)
不应该 依赖于 底层低阶模组 (callee)
,两者都该 依赖抽象介面
。
原因
避免 上层高阶模组 (caller)
因为 底层低阶模组 (callee)
改变而被迫改变,导致更多 bug
参考资料
- 09. 物件导向设计原则—SOLID - iT 邦帮忙::一起帮忙解决难题,拯救 IT 人的一天
- 搞笑谈软工: SOLID:五则皆变
- 浅谈物件导向 SOLID 原则对工程师的好处与如何影响能力 - iT 邦帮忙::一起帮忙解决难题,拯救 IT 人的一天
- SOLID 里氏替换原则 Liskov Substitution Principle (LSP) - Finn - Medium
- S.O.L.I.D 五大原则之里氏替换原则 LSP - 深入理解 JavaScript - 极客学院Wiki
- [Day05] 里氏替换原则 | Liskov Substitution Principle - iT 邦帮忙::一起帮忙解决难题,拯救 IT 人的一天