Java設計模式(2:單一職責原則和依賴倒置原則詳解)

一、單一職責原則

不要存在多於一個導致類變更的原因。簡單來說,就是一個Class/Interface/Method只負責一項職責

這句話最為重要的就是這一段:一個Class/Interface/Method只負責一項職責

我們先來舉一個例子,我們在日常生活中都或多或少的聽過LOL(英雄聯盟)這個遊戲,而這個遊戲在各個直播平台都很火爆,那我們就以此為例:

某個遊戲直播平台會將主播直播時的影片錄製下來,等到主播下播後再上傳到平台,這樣就形成了錄播。對於這兩種影片觀看模式,平台有著這樣的規定:觀看直播不允許快進、回放,而錄播則可以,那我們首先想到的應該是這樣方式:

/**
 * 平台
 */
public class UuSee {

    private final String LiveName = "直播";
    
     private final String ReplayName = "錄播";

    // 觀看
    public void watch(String name){
        if (LiveName.equals(name)){
            System.out.println("觀看LOL "+name+",不能快進、回放!");
        } else if(ReplayName.equals(name)) {
            System.out.println("觀看LOL "+name+",可以快進、回放!");
        }
    }
}

我們來寫一個測試的程式碼看看:

public static void main(String[] args) {
    UuSee uuSee = new UuSee();
    uuSee.watch("直播");
    uuSee.watch("錄播");
}

從測試的程式碼來看的話,UuSee類承擔了兩種不同的處理邏輯。那麼現在來增加一個需求:對直播和錄播的影片進行加密,而直播和錄播影片的加密方式不同。那麼我們必須要修改源碼,而這可能影響其他地方的調用。所以現在我們來將兩種觀看方式分開,讓它們互不干擾:

直播(LiveVideo類):

/**
 * 直播
 */
public class LiveVideo {
    
    public void watch(String name){
        System.out.println("直播影片加密......");
        System.out.println("觀看LOL "+name+",不能快進....");
    }
    
}

錄播(RePlayVideo類):

/**
 * 錄播
 */
public class RePlayVideo {

    public void watch(String name){
        System.out.println("錄播影片加密......");
        System.out.println("觀看LOL "+name+",可以快進.....");
    }
    
}

調用程式碼:

public static void main(String[] args) {
    RePlayVideo rePlayVideo = new RePlayVideo();
    rePlayVideo.watch("錄播");
    
    LiveVideo liveVideo = new LiveVideo();
    liveVideo.watch("直播");
}

這樣看的話,直播類LiveVideo和錄播類RePlayVideo都調用自己的處理邏輯,兩者互不干擾。那麼如果業務繼續發展:增加VIP用戶,並且只有VIP用戶才能觀看錄播(獲得影片流);而普通用戶雖然不能觀看錄播,但可以獲取錄播影片的基本資訊。這樣就會增加兩個方法:getReplayVideoInfo()getReplayVideo(),這時候發現錄播類RePlayVideo和直播類LiveVideo都會擁有其中的兩個方法,那不如設計一個頂級介面,將他們一起納入管理:UuSeeInterface

public interface UuSeeInterface {

    // 獲得 錄播影片資訊
    public String getReplayVideoInfo();

    // 獲得 錄播影片 影片流
    public byte[] getReplayVideo();
    
    // 觀看影片
    public void watch(String name);
}

寫完這個介面,我們發現從控制影片播放許可權的層面來看的話,可以分為兩個職責:管理職責和展示職責;getReplayVideoInfo()getReplayVideo()方法都屬於展示職責,watch()方法屬於管理職責,這樣的話我們就可以將上面的介面拆分一下:

管理職責介面(UuSeeManagement

/**
 * 管理職責
 */
public interface UuSeeManagement {
    // 觀看影片
    public void watch(String name);
}

展示職責介面(UuSeeInfo

/**
 * 展示職責
 */
public interface UuSeeInfo {

    // 獲得 錄播影片資訊
    public String getReplayVideoInfo();

    // 獲得 錄播影片 影片流
    public byte[] getReplayVideo();
}

說完了一個Class/Interface只負責一項職責的事情後,我們再來看一看一個Method只負責一項職責的問題。

假如有這麼一個方法:更改一個用戶的用戶名和地址,我們可能會偷懶寫成這樣。

// 修改用戶名稱和地址
public void modifyUserInfo(String userName,String address){
    System.out.println("用戶名改為:"+userName);
    System.out.println("用戶地址改為:"+address);
}

那麼當我們又增加一個需求:只更改用戶名稱,不更改用戶地址。那麼我們在調用modifyUserInfo()方法時,還要去千方百計的獲得用戶的地址,非常的麻煩,倒不如將上面的方法拆分為兩個方法:

// 修改用戶名稱
public void modifyUserName(String userName){
    System.out.println("用戶名改為:"+userName);
}

// 修改用戶地址
public void modifyUserAddress(String address){
    System.out.println("用戶地址改為:"+address);
}

這樣來看,我們需要修改用戶名稱的時候就調用modifyUserName()方法,需要修改用戶地址的時候就調用modifyUserAddress()方法,兩個方法獨立分開,不相互干擾,後期也好維護。

當然,在日常工作中,我們不可能面面俱到,也不要為了某種原則而將自己陷入某個陷阱中,還是要根據實際情況做出不同的選擇。但是,編寫程式碼時,要盡量的滿足單一原則,這樣也方便我們後期的程式碼維護。

二、依賴倒置原則

這個原則是開閉原則的基礎,是指設計結構程式碼時,高層模組不應該依賴於底層模組,二者應該依賴於抽象。抽象不應該依賴於細節,細節應該依賴於抽象。即:針對介面編程,依賴於抽象而不依賴於具體

我們來先看一段程式碼:

/**
 * 動物園
 */
public class Zoom {

    // 老虎
    public void tiger(){
        System.out.println("老虎在吃雞肉.....");
    }

    // 猴子
    public void monkey(){
        System.out.println("猴子在吃香蕉.....");
    }
}
// 測試
public static void main(String[] args) {
    Zoom zoom = new Zoom();
    zoom.tiger();
    zoom.monkey();
}

小明周末去動物園遊玩,正值晌午,動物飼養員在給動物餵食,覺得有趣就想記錄一下。在記錄完tiger和monkey之後,小明又發現了熊貓(panda)在吃竹子,於是興沖沖的準備記錄下來,可動筆時卻發現一個問題:tiger()monkey()方法已經寫好了,調用沒有任何問題,但在增加一個panda()方法的話,不就修改了Zoom類的源程式碼,這樣會不會有額外的風險呢?似乎也不符合設計模式中的開閉原則。思考良久,小明將上述程式碼改成了下面這樣:

/**
 * 動物園
 */
public interface Zoom {

    // 吃午飯
    public void eat();
}
// 老虎
public class Tiger implements Zoom {
    @Override
    public void eat() {
        System.out.println("老虎在吃雞肉.....");
    }
}
// 猴子
public class Monkey implements Zoom{
    @Override
    public void eat() {
        System.out.println("猴子在吃香蕉......");
    }
}
// 調用類
public class Xming {
    public void eat(Zoom zoom){
        zoom.eat();
    }
}

// 測試
public static void main(String[] args) {
        Xming xming = new Xming();
        xming.eat(new Tiger()); // 老虎
        xming.eat(new Monkey()); // 猴子
    }

這樣一看,TigerMonkey互不干擾,當需要記錄熊貓吃竹子時,只需要在創建一個Panda類就可以了,不用去修改現有的源程式碼:

// 熊貓
public class Panda implements Zoom {
    @Override
    public void eat() {
        System.out.println("熊貓正在吃竹子......");
    }
}

// 測試
public static void main(String[] args) {
        Xming xming = new Xming();
        xming.eat(new Tiger()); // 老虎
        xming.eat(new Monkey()); // 猴子
        xming.eat(new Panda());// 熊貓
    }

這個時候我們發現Xming類調用eat()方法時注入方式很熟悉,想了想是依賴注入,而依賴注入的方式還有構造器注入setter注入。我們先來看看構造器注入的方式:

// 調用類
public class Xming {
    
    private Zoom zoom;

    public Xming(Zoom zoom) {
        this.zoom = zoom;
    }

    public void eat(){
        zoom.eat();
    }
}

調用程式碼:

public static void main(String[] args) {
    Xming tiger = new Xming(new Tiger());
    tiger.eat();
    Xming panda = new Xming(new Panda());
    panda.eat();
}

構造器注入的方式在每次調用時都要創建實例。那麼,如果Xming是全局單例的話,我們就只能選擇setter注入的方式了:

// 調用類
public class Xming {

    private Zoom zoom;

    public void setZoom(Zoom zoom) {
        this.zoom = zoom;
    }

    public void eat(){
        zoom.eat();
    }
}
// 調用
public static void main(String[] args) {
    Xming tiger = new Xming();
    tiger.setZoom(new Tiger());
    tiger.eat();
    Xming panda = new Xming();
    panda.setZoom(new Panda());
    panda.eat();
}

結語:依賴倒置原則的本質還是面向介面編程,而事實就是以抽象為基準比以細節為基準搭建起來的框架要穩定的多,後期維護與查看都相對的容易與清晰一些。所以,我們在日常的開發中,要根據實際的業務需求來分析,盡量的使用面向介面編程,先頂層再細節的步驟來設計程式碼架構。