物件导向设计的五原则 SOLID

程式进阶架构知识 Coding Structure 物件导向设计的五原则 SOLID

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 的优缺点

优点

  1. 提高程式码的 重用性
  2. 提高类别的 扩充性

缺点

  1. 因为继承关係,耦合性增高 (修改父类别常数时需要思考是否会影响到其他继承的子类别)
  2. 降低程式码灵活性 (必须实作父类别所有方法)

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

参考资料