經典設計原則 – SOLID

SOLID 設計原則包含以下 5 種原則:

  • 單一職責原則(Single Responsibility Principle, SRP)
  • 開閉原則(Open Closed Principle, OCP)
  • 里式替換原則(Liskov Substitution Principle, LSP)
  • 介面隔離原則(Interface Segregation Principle, ISP)
  • 依賴反轉原則(Dependency Inversion Principle, DIP)

單一職責原則

理解

單一職責原則的描述是,一個類或者模組只負責完成一個職責(或功能)。當然,單一職責原則不止是可以針對於模組或類,對於很多粒度都有效果,如函數、類、介面、模組等等,模組通常由多個類組成。

職責可以指模組變化的原因,從這個角度理解,單一職責原則表示不要存在超過一個導致模組變更的原因。

需要注意的是,不同的應用場景、不同階段的需求背景、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。

優點

遵循單一職責原則,將會有以下的優點:

  • 提高程式碼的可維護性:職責越少,複雜度越低,可讀性更好,可維護性就更高
  • 降低程式碼變更的風險:職責越多,程式碼變更的可能性就越高,變更帶來的風險也就越大

最佳實踐

在實際開發中,出現以下現象有可能違反了單一職責原則:

  • 模組的變數、屬性或程式碼行數過多
  • 模組的內部對外部依賴過多
  • 模組的私有方法過多
  • 難以給模組取一個合理的名稱
  • 模組的大部分操作只針對幾個屬性

如出現上述情況,則需要判斷是否對程式碼做職責分離,以遵循單一職責原則,最終應以提高內聚、降低耦合、保證程式碼的可維護性為主。

開閉原則

理解

開閉原則的描述是,軟體實體(模組、類、方法等)應該「對擴展開放、對修改關閉」。

詳細的解釋就是,添加一個新的功能時,在已有程式碼基礎上擴展程式碼(新增模組、類、方法等),而非修改已有程式碼(修改模組、類、方法等)。更寬鬆的理解是以最小的修改程式碼的代價來完成新功能的開發。

優點

遵循開閉原則,將會有以下的優點:

  • 減少測試範圍:修改的程式碼範圍越小,涉及的測試範圍越小,未改動的測試程式碼仍能正常運行
  • 降低維護成本:軟體規模越大、壽命越長,則軟體的維護成本越高

最佳實踐

若要做到「對擴展開發、對修改關閉」,有以下幾點需要注意:

  • 時刻具備擴展意識、抽象意識、封裝意識,多花時間設計程式碼結構,事先留好擴展點
  • 大部分經典設計模式都是為了解決程式碼的擴展性問題而總結出來的,開閉原則是它們一個重要的評價依據

里式替換原則

理解

里式替換原則的描述是,子類對象能夠替換程式中父類對象出現的任何地方,並且保證原來程式的邏輯行為不變及正確性不被破壞。

從程式碼實現上看,面向對象的多態和里式替換原則有點類似,但是它們的關注點不一樣:里式替換原則是用來指導繼承關係中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程式的邏輯以及不破壞原有程式的正確性。

優點

遵循里式替換原則,將會有以下的優點:

  • 實現有意義的繼承:保證了父類的復用性,也降低了系統出錯誤的故障,防止誤操作,同時也不會破壞繼承的機制
  • 增強程式的健壯性:不同的子類可以完成不同的業務邏輯,即使增加子類也能保持非常好的兼容性

最佳實踐

通常,需要注意以下違反里式替換原則的程式碼:

  • 子類違背父類聲明要實現的功能,如將加法改成減法
  • 子類違背父類對輸入、輸出、異常的約定,如同一情況拋出的異常不同等
  • 子類違背父類注釋中所羅列的任何特殊聲明

介面隔離原則

理解

介面隔離原則的描述是,介面的調用者或使用者不應該被強迫依賴它不需要的介面。

通過對介面的理解不同,介面隔離原則有以下三種理解:

1、如果把「介面」理解成一組介面集合,可以是某個微服務的介面,也可以是某個類庫的介面等。如果存在部分介面只被部分調用者使用,就需要將這部分介面隔離出來,單獨給這部分調用者使用,而不強迫其他調用者也依賴其他不會用到的介面。

2、如果把「介面」理解成單個 API 介面或函數,部分調用者只需要其中的部分功能,則需要將這個函數拆分成更細粒度的多個函數,讓調用者只依賴它需要的那個細粒度函數。

3、如果把「介面」理解成 OOP 中的介面,也可以理解成為面向對象程式語言中的介面語法,那介面的設計要盡量單一,不要讓介面的實現類和調用者依賴不需要的介面函數。

介面隔離原則和單一職責原則有點類似,但介面隔離原則更側重於介面的設計,通常是通過調用者如何使用介面來定義這個介面的設計是否足夠職責單一。

優點

遵循介面隔離原則,將會有以下的優點:

  • 高內聚,低耦合:拆分成更小粒度的介面,減少對外的交互,預防外來的變更,提高系統的靈活性和可維護性
  • 可讀性高,易於維護:合理的介面拆分粒度能保證系統的穩定性,減少項目工程的程式碼冗餘

最佳實踐

採用介面隔離原則對介面進行約束時,要注意以下幾點:

  • 介面盡量小,但是要有限度。定義過小,則會造成介面數量過多,使設計複雜化;定義多大,靈活性降低
  • 每個項目和產品都有選定的環境因素,環境不同,介面拆分的標準就不同,深入了解業務邏輯

依賴反轉原則

理解

依賴反轉原則也被叫作依賴倒置原則,其含義是:高層模組不要依賴底層模組,高層模組和底層模組應該通過抽象來互相依賴;抽象不要依賴具體實現細節,具體實現細節依賴抽象。

Tomcat 是運行 Java Web 應用程式的容器,編寫的 Web 應用程式程式碼只需要部署在 Tomcat 容器中下,便可被 Tomcat 容器調用執行。在這裡,Tomcat 容器就是高層模組,Web 應用程式就是底層模組。Tomcat 容器和 Web 應用程式沒有直接的依賴關係,而是通過 Servlet 規範實現互相依賴,而 Servlet 規範也不會依賴具體的實現細節,而是 Tomcat 和 Web 應用程式依賴 Servlet 規範。

控制反轉

控制反轉(Inversion Of Control, IoC)指的是將程式設計師自己對程式執行流程的控制反轉成通過框架控制。控制反轉並不是一種具體的設計技巧,而是一種籠統的設計思想,一般用來指導框架層面的設計。

實現控制反轉主要有兩種方式:依賴注入和依賴查找。兩者的區別在於,前者是被動的接收對象,在類 A 的實例創建過程中即創建了依賴的 B 對象,通過類型或名稱來判斷將不同的對象注入到不同的屬性中,而後者是主動索取相應類型的對象,獲得依賴對象的時間也可以在程式碼中自由控制。

依賴注入

依賴注入(Dependency Injection, DI)是一種具體的編碼技巧。

其詳細概括就是:不通過 new 的方式在類的內部創建依賴對象,而是將依賴的類對象在外部創建好之後,通過構造函數、函數參數等方式傳遞(或注入)給類使用。

一個簡單的依賴注入程式碼例子如下:

package cn.fatedeity.designpattern.philosophy;

/**
 * 依賴注入案例
 */
public class DependencyInjectionCase {
    private MessageSender messageSender;

    public DependencyInjectionCase(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void sendMessage(String phone, String message) {
        this.messageSender.send(phone, message);
    }

    public static void main(String[] args) {
        MessageSender smsSender = new SmsSender();
        DependencyInjectionCase dependencyInjectionCase0 = new DependencyInjectionCase(smsSender);
        // SmsSender sms send sms message
        dependencyInjectionCase0.sendMessage("sms", "sms message");

        MessageSender inboxSender = new InboxSender();
        DependencyInjectionCase dependencyInjectionCase1 = new DependencyInjectionCase(smsSender);
        // SmsSender inbox send inbox message
        dependencyInjectionCase1.sendMessage("inbox", "inbox message");
    }
}

class InboxSender implements MessageSender {
    @Override
    public void send(String phone, String message) {
        System.out.println("InboxSender " + phone + " send "+ message);
    }
}

class SmsSender implements MessageSender {
    @Override
    public void send(String phone, String message) {
        System.out.println("SmsSender " + phone + " send "+ message);
    }
}

interface MessageSender {
    void send(String phone, String message);
}

優點

遵循依賴反轉原則,將會有以下的優點:

  • 查詢依賴和應用程式碼分離,大量降低工廠類和單例類的數量,程式碼層次更加清晰
  • 沒有侵入性,無須依賴容器的 API,也無須實現一些特殊介面

最佳實踐

通過依賴注入提供的擴展點,簡單配置一下所有需要的類及其類之間依賴關係,就可以實現由框架來自動創建對象、管理對象的生命周期、依賴注入等功能。

現成的依賴注入創建有很多,比如 Google Guide、Java Spring、Pico Container、Butterfly Container 等。