SOLID原則的堅實指南| Baeldung

banq發表於2019-02-06

在本教程中,我們將討論物件導向設計的SOLID原則。
首先,我們將首先探討它們出現的原因以及為什麼在設計軟體時應該考慮它們。然後,我們將概述每個原則以及一些示例程式碼以強調這一點。

SOLID原則的原因
SOLID原則首先由Robert C. Martin在2000年的論文“ 設計原則和設計模式”中概念化  。 這些概念後來由Michael Feathers構建,他們向我們介紹了SOLID的首字母縮略詞。在過去的20年中,這5個原則徹底改變了物件導向程式設計的世界,改變了我們編寫軟體的方式。
那麼,什麼是SOLID以及它如何幫助我們編寫更好的程式碼?簡而言之,Martin和Feathers的設計原則鼓勵我們建立更易於維護,易懂且靈活的軟體。因此,隨著我們的應用程式規模不斷擴大,我們可以降低其複雜性,併為您節省更多的麻煩!

以下5個概念構成了我們的SOLID原則:

  1. Single Responsibility單一職責
  2. Open/Closed開閉原則
  3. Liskov Substitution
  4. Interface Segregation介面分離
  5. Dependency Injection依賴注射

雖然其中一些詞聽起來令人生畏,但可以透過一些簡單的程式碼示例輕鬆理解它們。在接下來的部分中,我們將深入探討每個原則的含義,以及一個快速的Java示例來說明每個原則。

1. 單一責任
讓我們用單一的責任原則來解決問題。正如我們所預料的那樣,這個原則規定一個類應該只有一個責任。此外,它只應該有一個改變的理由。
這個原則如何幫助我們構建更好的軟體?讓我們看看它的一些好處:

  1. 測試  - 具有一個職責的類將具有少得多的測試用例
  2. 較低的耦合  - 單個類中較少的功能將具有較少的依賴性
  3. 組織  - 較小,組織良好的類比單片類更容易搜尋

舉一個例如一個類來代表一本簡單的書:

public class Book {
 
    private String name;
    private String author;
    private String text;
 
    //constructor, getters and setters
}

在此程式碼中,我們儲存與Book例項關聯的名稱,作者和文字。
現在讓我們新增幾種查詢文字的方法:

public class Book {
 
    private String name;
    private String author;
    private String text;
 
    //constructor, getters and setters
 
    // methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }
 
    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

現在,我們的Book  類運作良好,我們可以在應用程式中儲存儘可能多的書籍。但是,如果我們無法將文字輸出到控制檯並閱讀它,那麼儲存資訊有什麼用?
新增列印方法:

public class Book {
    //...
 
    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}


但是,此程式碼違反了我們之前概述的單一責任原則。為了修復我們的混亂,我們應該實現一個單獨的類,只關注列印我們的文字:

public class BookPrinter {
 
    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }
 
    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

真棒。我們不僅開發了一個新類來減輕  Book 的列印職責,而且還可以利用我們的  BookPrinter 類將文字傳送到其他媒體。
無論是電子郵件,日誌記錄還是其他任何內容,我們都有一個專門針對這一問題的類。

2. 開閉原則
類應該是可以擴充套件的,但是對修改是關閉, 我們應該停止修改現有程式碼,否則會在經過測試的應用程式中引發潛在的新錯誤。當然,規則的一個例外是:修復現有程式碼中的錯誤。
讓我們透過快速程式碼示例進一步探索這個概念,作為新專案的一部分,想象一下我們已經實現了一個  吉他 類。

public class Guitar {
 
    private String make;
    private String model;
    private int volume;
 
    //Constructors, getters & setters
}

我們推出這個應用程式,每個人都喜歡它。然而,幾個月後,我們認為  吉他 有點無聊,可以用一個很棒的火焰模式讓它看起來更“搖滾”。
在這一點上,可能很容易開啟  Guitar 類並新增一個火焰模式 - 但是誰知道在我們的應用程式中可能會出現什麼錯誤。
相反,讓我們堅持開放式原則並簡單地擴充套件我們的  吉他 類(banq注:擴充套件類不代表一定要繼承,可以使用組合方式):

public class SuperCoolGuitarWithFlames extends Guitar {
 
    private String flameColor;
 
    //constructor, getters + setters
}


透過擴充套件  Guitar 類,我們可以確保我們現有的應用程式不會受到影響。


3. Liskov替代
這可能是5個原則中最複雜的。簡單地說,如果A類是B類的子型別,那麼我們應該能夠用  A 替換  B 而不破壞我們程式的行為。讓我們直接跳轉到程式碼,以幫助我們圍繞這個概念:

public interface Car {
 
    void turnOnEngine();
    void accelerate();
}

在上面,我們定義了一個簡單的  Car 介面,其中包含了所有汽車應該能夠實現的幾種方法 - 開啟引擎並加速前進。
讓我們實現我們的介面併為方法提供一些程式碼:

public class MotorCar implements Car {
 
    private Engine engine;
 
    //Constructors, getters + setters
 
    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }
 
    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}


正如我們的程式碼描述的那樣,我們有一個可以開啟的引擎,我們可以增加功率。但等一下,2019年,埃隆馬斯克一直是一個忙碌的人。
我們現在生活在電動汽車時代:

public class ElectricCar implements Car {
 
    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }
 
    public void accelerate() {
        //this acceleration is crazy!
    }
}

將沒有引擎的電車放進一個混合物物件中,就改變了我們程式的行為。這是對Liskov替換的公然違反,並且比我們之前的2條原則更難修復。
一種可能的解決方案是將我們的模型重新設計為考慮到無引擎狀態的  汽車的介面。

4.介面隔離
SOLID中的“I”代表介面隔離,它只是意味著更大的介面應該分成更小的介面。透過這樣做,我們可以確保實現類只需要關注它們感興趣的方法。
對於這個例子,我們將嘗試作為動物園管理員。更具體地說,我們將在熊圈中工作。
讓我們從一個介面開始,概述我們作為熊守護者的角色:

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

作為狂熱的動物園管理員,我們非常樂意為我們心愛的熊洗淨和餵食。然而,我們都非常清楚撫摸它們的危險。不幸的是,我們的介面相當大,我們別無選擇,只能實施程式碼來承擔責任。
讓我們透過將我們的大型介面分成3個獨立的介面來解決這個問題:

public interface BearCleaner {
    void washTheBear();
}
 
public interface BearFeeder {
    void feedTheBear();
}
 
public interface BearPetter {
    void petTheBear();
}

現在,由於介面隔離,我們可以自由地只實現對我們重要的方法:

public class BearCarer implements BearCleaner, BearFeeder {
 
    public void washTheBear() {
        //I think we missed a spot...
    }
 
    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}


最後,我們可以把危險的東西留給瘋狂的人:

public class CrazyPerson implements BearPetter {
 
    public void petTheBear() {
        //Good luck with that!
    }
}


更進一步,我們甚至可以將我們的BookPrinter 類從之前的示例拆分  為以相同的方式使用介面隔離。透過使用單個列印 方法實現  Printer介面  ,我們可以例項化單獨的  ConsoleBookPrinter 和  OtherMediaBookPrinter 類。

5.依賴注入
當有人提到“依賴注入”這個詞時,可能會想到一些框架 - 谷歌的Guice,或者也許是Spring。但事實是,我們不需要複雜的框架來理解這個原則。
依賴注入只是在建立時注入類的依賴關係的技術,避免了危險的  新 關鍵字。
為了證明這一點,讓我們使用程式碼實現Windows 98計算機:

public class Windows98Machine {}


但沒有顯示器和鍵盤的電腦有什麼用?讓我們在建構函式中新增其中一個,以便我們例項化的每個  Windows98Computer 都預裝了一個Monitor 和一個  鍵盤:

public class Windows98Machine {
 
    private final Keyboard keyboard;
    private final Monitor monitor;
 
    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new Keyboard();
    }
 
}

這段程式碼可以使用,我們將能夠在Windows98Computer 類中自由  使用鍵盤和監視器。問題解決了?不完全的。透過使用  new 關鍵字宣告  Keyboard 和  Monitor ,我們將這3個類緊密耦合在一起。
這不僅使我們的  Windows98Computer 難以測試,而且我們也失去了在需要時用子類切換我們的Keyboard 類的能力  。我們也耦合使用了我們的Monitor類。
讓我們看看當我們應用一些簡單的依賴注入時,同一個例子是如何看的:

public class Windows98Machine{
 
    private final Keyboard keyboard;
    private final Monitor monitor;
 
    public Windows98Machine(Keyboard keyboard, Monitor monitor) {
        this.keyboard = keyboard;
        this.monitor = monitor;
    }
}


優秀!我們已經解耦了依賴關係,並可以使用我們選擇的任何測試框架自由測試我們的  Windows98Machine 。(banq注:可以使用builder模式實現兩個輸入引數的注入)

結論
在本教程中,我們深入探討了物件導向設計的SOLID原則。
我們從一小段SOLID歷史開始,以及這些原則存在的原因。
我們逐字逐字地用一個違反它的快速程式碼示例來分解每個原則的含義。然後,我們瞭解瞭如何修復程式碼 並使其符合SOLID原則。

程式碼在GitHub可用

相關文章