一看就懂的設計模式——六大原則(02)

2020-08-14 19:09:35

單一職責原則

單一職責原則的英文是 Single Responsibility Principle,縮寫爲 SRP。這個原則的英文描述是這樣的:A class or module should have a single reponsibility。一個類或者模組只負責完成一個功能。

從定義可知,在開發中不要設計大而全的類,要設計粒度小、功能單一的類。如果一個類包含了兩個或者兩個以上業務不相乾的功能,那我們就說它職責不夠單一,應該將它拆分成多個功能更加單一、粒度更細的類。

單一職責原則的優點:

  • 類的複雜性降低,實現什麼職責都有明確的定義;
  • 邏輯變得簡單,類的可讀性提高了,而且,因爲邏輯簡單,程式碼的可維護性也提高了;
  • 變更的風險降低,因爲只會在單一的類中的修改。

舉個例子:
我們在做一個專案需要管理使用者資訊,我們有一個使用者資訊的類:

public class User{
	//修改使用者資訊
	public void Update(Userinfo userinfo){}
	//禁用某個使用者
	public void DisableByUID(int uid){}
}

如果這時需要增加一個給客戶發送郵件通知的功能,可能會寫成下面 下麪這樣

//錯誤的例子
public class User{
	//修改使用者資訊
	public void Update(Userinfo userinfo){}
	//禁用某個使用者
	public void DisableByUID(int uid){}
	//發送郵件通知
	public void SendEmail(){}
}

這種寫法就給User類增加了過多的功能,也不利於後期維護和複用,比方說管理員也需要增加發送郵件通知,比方說需要再增加發送簡訊通知,所以符合單一職責的寫法就是將通知功能獨立到一個類中

//使用者類
pubilc class User{
	//修改使用者資訊
	public void Update(Userinfo userinfo){}
	//禁用某個使用者
	public void DisableByUID(int uid){}
}
//通知類
public class Notification{
	//發送訊息
	//MessageType 1:簡訊,2:郵件
	public void SendMessage(int MessageType, Message message){}
}
//訊息體
public class Message{
	//發送人
	public string SendFrom {get;set;}
	//接受人
	public string SendTo{get;set;}	
	//訊息內容
	public string Content{get;set;}
	//發送時間
	public datetime SentTime{get;set;}
}

開閉原則

一個軟體實體如類、模組和函數應該對擴充套件開放,對修改關閉

一個軟體實體應該通過擴充套件來實現變化,而不是通過修改已有的程式碼實現變化。之所以不建議修改已有程式碼,是因爲一旦對原有的程式碼進行修改,就有可能影響到原有的模組,引起bug,修改完還需要對專案中所有涉及到該模組的功能進行測試,徒增工作量。讓物件對擴充套件開放,這要求我們要對需求的變更有一定的前瞻性和預見性,在實現層面,讓程式碼依賴於抽象而非細節。比方說我們做的專案是使用SqlServer數據庫,但我們知道該專案後期也可能更換成mysql數據庫,就可以向下面 下麪這樣實現

interface IDatabase{
	void Insert();
	void Delete();
	void Update();
	void Select();	
}

public class SqlServerDB : IDatabase{
	public void Insert();
	public void Delete();
	public void Update();
	public void Select();
}

public class MySqlDB : IDatabase{
	public void Insert();
	public void Delete();
	public void Update();
	public void Select();
}

//業務層在呼叫時,依賴抽象,也可以通過設定參數來實現範例化不同的數據庫操作類
IDatabase db = new SqlServerDB();
db.Delete();

裡氏替換原則

如果對每一個型別爲T1的物件o1,都有型別爲T2的物件o2,使得以T1定義的所有程式P在所有物件o1都替換成o2的時候,程式P的行爲都沒有發生變化,那麼型別T2是型別T1的子型別。

說人話就是:

只要父類別能出現的地方子類就可以出現。

這個原則定義了4層含義:

  1. 子類可以實現父類別的抽象方法,但是不能覆蓋父類別的非抽象方法。
  2. 子類可以有自己的個性,可以有自己的屬性和方法。
  3. 子類覆蓋或過載父類別的方法時輸入參數可以被放大。
  4. 子類覆蓋或過載父類別的方法時輸出結果可以被縮小,也就是說返回值要小於或等於父類別的方法返回值。

在軟件開發過程中,子類替換父類別後,程式的行爲應該是一樣的。只有當子類替換掉父類別後,此時軟體的功能不受影響,父類別才能 纔能真正地被複用,子類才能 纔能在父類別的基礎上新增新的行爲。下面 下麪是一個錯誤的示範:

public class C {
    public int func(int a, int b){
        return a+b;
    }
}
  
public override class C1 : C{
    public int func(int a, int b) {
        return a-b;
    }
}
  
public class Client{
    public static void main(String[] args) {
        C c = new C1();
        Console.WriteLine("2+1=" + c.func(2, 1));
    }
}

執行結果:2+1=1
子類重寫了父類別C的func方法,導致參照父類別的地方並不能透明的使用子類的物件,違背裡氏替換原則。正確的做法是在子類中重新寫一個心的方法:

public class C {
    public int func(int a, int b){
        return a+b;
    }
}
  
public class C1 : C{
    public int func2(int a, int b) {
        return a-b;
    }
}
  
public class Client{
    public static void main(String[] args) {
        C1 c = new C1();
        Console.WriteLine("2-1=" + c.func2(2, 1));
    }
}

依賴倒置原則

高層模組不應該依賴底層模組,兩者都應該依賴其抽象;
抽象不應該依賴細節;
細節應該依賴抽象;

在C#中點抽象指的是介面和抽象類,兩者都是不能直接被範例化的;細節就是實現類,實現介面或繼承抽象類而產生的類就是細節,特點就是可以直接被範例化。所以該原則可以這麼理解:

模組間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過介面或抽象類產生的
介面或抽象類不依賴於實現類
實現類應該依賴介面或抽象類

依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象爲基礎搭建起來的架構比以細節爲基礎搭建起來的架構要穩定的多。依賴倒置原則的核心思想是面向介面程式設計,如果理解了面向介面程式設計的概念也就自然掌握依賴倒置原則,網上有個讀書讀報紙的例子很形象的解釋了該原則:

class Book{
	public String getContent(){
		return "從前有座山,山裏有個廟……";
	}
}
 
class Mother{
	public void narrate(Book book){
		Console.WriteLine("媽媽開始講故事");
		Console.WriteLine(book.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
}

如果現在要增加讀報紙的功能

class Newspaper{
	public String getContent(){
		return "今日熱點新聞";
	}
}

這時不得不修改Mother類才能 纔能讀報紙,這樣不科學,所以我們引入介面的概念來重構這段程式碼


interface IReader{
	public String getContent();
}

定義個讀的介面,然後讓讀書和讀報紙都繼承該介面,此時Mother就能讀各種內容了

class Newspaper implements IReader {
	public String getContent(){
		return "今日熱點新聞";
	}
}
class Book implements IReader{
	public String getContent(){
		return "從前有座山,山裏有個廟……";
	}
}
 
class Mother{
	public void narrate(IReader reader){
		Console.WriteLine("媽媽開始講故事");
		Console.WriteLine(reader.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}

介面隔離原則

用戶端不應該依賴它不需要的介面,類間的依賴關係應該建立在最小的介面之上

介面隔離原則將非常龐大、臃腫的介面拆分成更小更具體的介面。一個介面如果定義的過於臃腫,則代表它的每一個實現類都要考慮所有的實現邏輯,無形增加了維護的成本。

舉例來說,所有雲服務供應商都與阿裡雲一樣提供相同種類的功能。但當你着手爲其他供應商提供支援時, 程式庫中絕大部分的 介面會顯得過於寬泛。其他雲服務供應商沒有提供部分方法 所描述的功能。
在这里插入图片描述
那騰訊雲中沒有介面定義的3個方法,這種情況就是違反了介面隔離原則,正確的做法是將介面拆分成更小的粒度,如下:
在这里插入图片描述

迪米特法則

一個物件應該對其他物件有最小的瞭解。如果兩個類不必彼此直接通訊,那麼這兩個類就應當發生直接的相互作用。如果其中一個類需要呼叫另一個類的某一個方法的話,可以通過第三者轉發這個呼叫。

迪米特法則強調的前提是在類的結構設計上,每一個類都應當儘量降低成員的存取許可權,也就是說,一個類包裝好自己的private狀態,不需要讓別的類知道的欄位或行爲就不要公開,其實就是在封裝一個類的時候儘量明確這個類的屬性和操作屬性的方法。迪米特法則的根本思想,是強調類自己的松耦合。類之間的松耦合越弱,越有利於複用。

來看個錯誤的例子:

public class LODErrorTest {

    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.readBook();
    }

}


/**
 * 錯誤的示範
 */
public class Phone {
    App app = new App();
    //關鍵是下面 下麪這行程式碼
    Book book = new Book("設計模式");
    public void readBook() {
        app.read(book);
    }

}


public class App {
    
    public void read(Book book) {
        Console.WriteLine(book.getTitle());
    }

}

public class Book {

    private String title;

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

在手機類中,手機和書籍是沒有關係的,書籍應該是在閱讀軟體裏面,所以應該將手機與書籍隔離開,正確的寫法是這樣子的:

public class LODRightTest {

    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.readBook();
    }

}

/**
 * 正確的示範
 */
public class Phone {

    private App app = new App();

    public void readBook() {
        app.read();
    }

}


public class App {
    private Book book = new Book("設計模式");

    public void read() {
        Console.WriteLine(book.getTitle());
    }

}

public class Book {

    private String title;

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}