解析設計模式與設計原則:構建可維護性和可延伸性程式碼的重要性

2023-10-17 12:01:17

本文分享自華為雲社群《深入解析設計模式與設計原則:構建可維護性和可延伸性程式碼的重要性》,作者: Lion Long。

一、為什麼需要設計模式?

1.1、設計模式的定義

設計模式大概有23種。

設計模式是指在軟體開發中,經過驗證的,用於解決在特定環境下,重複出現的,特定問題的解決方案。

從定義可以看出,設計模式的使用有很多的侷限性。一定要明確它解決什麼問題,再使用它。當不清楚設計模式解決什麼問題時不要輕易使用。

通俗的講,設計模式是解決軟體開發過程中一些問題的固定套路。不要過度的封裝或使用設計模式,除非明確了需求的具體變化方向,而且變化方向的點是反覆的出現,才會使用設計模式;即慎用設計模式。

設計模式要到達一定的工程程式碼量才能精通。但是,瞭解設計模式是需要的。

1.2、設計模式由來

設計模式的由來可以追溯到20世紀80年代,由電腦科學家埃裡希·伽瑪(Erich Gamma)等人首次提出。他們將設計模式定義為可重複利用的解決方案,用於常見問題和設計挑戰。

設計模式的出現是為了解決軟體開發中的一些常見問題,幫助開發人員更高效地編寫可維護和可延伸的程式碼。通過使用設計模式,開發人員可以借鑑先前的成功經驗,避免重複發明輪子,同時提高程式碼的可讀性和可理解性。

設計模式的目標是提供經過驗證和經過時間考驗的解決方案,以解決特定情境中的常見問題。設計模式不是一種具體的演演算法或程式碼片段,而是一種在特定情境下的解決方案模板。它們可以應用於各種程式語言和開發環境中。

設計模式通常分為三種型別:建立型模式、結構型模式和行為型模式。

  • 建立型模式關注物件的建立機制;
  • 結構型模式關注物件之間的關係和組織方式;
  • 行為型模式關注物件之間的互動和通訊。

一些常見的設計模式包括單例模式、工廠模式、觀察者模式、策略模式等。

一句話來說,就是:滿足設計原則後,慢慢迭代出來的。

1.3、 設計模式解決的問題

使用設計模式的前提條件:具體的需求既有穩定點又有變化點。

(1)穩定點,即不會變的東西。如果全是穩定點,不需要設計模式。

(2)變化點,即經常發生變化。如果全是變化點,發生的改變沒有具體的方向,這也不需要設計模式。比如遊戲開發,使用指令碼語言解決全是變化的點,因為指令碼不需要重新編譯,熱更新就可以。

設計模式具體解決問題的場景:希望修改少量的程式碼,就可以適應需求的變化。比如,整潔的房間有一個好動的貓,如何保證房間的整潔?把貓關到籠子中,使貓在有限範圍內活動。

也就是使用設計模式,讓變化點在有限範圍內變化。

二、設計模式基礎

設計模式和開發語言相關的,利用語言的特性實現設計模式。

對於C++而言,設計模式的基礎是:

(1)物件導向的思想。物件導向的三個特徵,封裝(目的是隱藏實現細節,實現模組化)、繼承(目的是希望無需修改原有類的基礎上,通過繼承來實現功能的擴充套件;C++可以多繼承)、多型(靜態多型是函數過載,同一個函數名但引數不同來同時表現出不同的形態;動態的多型是繼承中虛擬函式的重寫)。設計函數很多依賴於動態的多型

(2)設計原則。

2.1、C++多型之虛擬函式重寫

假設一個基礎類別,有兩個虛擬函式:

class Base{
    public:
        virtual void func1(){}
        virtual void func2(){}
        int a;
};

其虛擬函式表和記憶體佈局為:

此時有一個子類繼承Base:

class Subject : public Base{
    public:
        virtual void func2(){}
        virtual void func3(){}
        int b;
};

其虛擬函式表和記憶體佈局為:

從記憶體佈局可以看到,有虛擬函式就會為該類生成虛擬函式表指標,虛擬函式表是編譯的時候編譯器自動幫我們自動生成的。虛擬函式表其實是一個一維陣列,陣列的元素儲存的虛擬函式地址,通過偏移就可以呼叫到相對應的函數。

對於Base類而言,虛擬函式表有func1和func2;Subject繼承Base,它的虛擬函式表中也會有Base的虛擬函式,而且虛擬函式表中Base的虛擬函式在Subject的虛擬函式前面。

如果Subject沒有重寫Base虛擬函式,那麼虛擬函式表中儲存的虛擬函式地址是一樣的(如範例中的func1)。

如果Subject重寫Base虛擬函式,那麼虛擬函式表中會發生替換,將Subject重新的虛擬函式地址替換掉Base中相應虛擬函式的地址(如範例中的func2)。

如果Subject自己有新的虛擬函式,則也要加入虛擬函式表中。

2.2、多型的體現

(1)早繫結。假如有Base *p=new Subject;如果Subject沒有重寫Base虛擬函式,那麼會將Subject型別轉換為Base型別,這就是早繫結。

(2)晚繫結。假如有Base *p=new Subject;如果Subject重寫了Base虛擬函式,那麼p實際指向的是Subject物件,這就是晚繫結。

2.3、擴充套件方式

(1)繼承。

(2)組合。

2.4、多型組合

// 繼承
class Subject : public Base{
};

// 組合
class Subject{
private:
    Base base;
};

設計模式中的組合通常是指組合基礎類別指標。好處是可以擴充套件Base的功能,通過多型方式讓組合解耦合。

// 組合基礎類別指標
class Subject{
private:
    Base *base;
};

三、設計原則

設計原則是設計模式還沒產生它就存在了。設計原則是多代程式設計師總結的開發原則。

3.1、依賴倒置

實現要依賴介面,介面又可以轉換為抽象,即具體實現的程式碼需要依賴這個抽象。具體使用介面(客戶)也要依賴這個抽象。

高層模組不應該依賴低層模組,兩者都應該依賴抽象;

抽象不應該依賴具體實現,具體實現應該依賴於抽象;

自動駕駛系統公司是高層,汽車生產廠商為低層,它們不應該互相依賴,一方變動另一方也會跟著變動;而應該抽象一個自動駕駛行業標準,高層和低層都依賴它;這樣以來就解耦了兩方的變動;自動駕駛系統、汽車生產廠商都是具體實現,它們應該都依賴自動駕駛行業標準(抽象)。

3.2、開放封閉

一個類應該對擴充套件(組合和繼承)開放,對修改關閉。針對封裝和多型。

3.3、面向介面

不將變數型別宣告為某個特定的具體類,而是宣告為某個介面;客戶程式無需獲知物件的具體型別,只需要知道物件所具有的介面;減少系統中各部分的依賴關係,從而實現「高內聚、鬆耦合」的型別設計方案;主要針對封裝。

3.4、封裝變化點

將穩定點和變化點分離,擴充套件修改變化點;讓穩定點和變化點的實現層次分離。主要針對封裝和多型。

3.5、單一職責

一個類應該僅有一個引起它變化的原因。主要針對封裝。

3.6、里氏替換

子型別必須能夠替換掉它的父類別型;主要出現在子類覆蓋父類別實現,原來使用父類別型的程式可能出現錯誤;覆蓋了父類別方法卻沒有實現父類別方法的職責。

主要針對多型中的虛擬函式重寫。

3.7、介面隔離

(1)不應該強迫客戶依賴於它們不用的方法;

(2)一般用於處理一個類擁有比較多的介面,而這些介面涉及到很多職責;

(3)使用者端不應該依賴它不需要的介面。一個類對另一個類的依賴應該建立在最小的介面上。

通過限定詞隔離。類與類之間依賴介面,通過介面隔離類。

3.8、組合優於繼承

繼承耦合度高,組合耦合度低。

3.9、最小知道原則

讓使用者儘量不選擇它不需要的介面。

總結

通過介紹設計模式的定義、分類和應用場景,以及設計原則的作用,強調了它們在軟體開發中的重要性。設計模式提供了可重複利用的解決方案,幫助開發人員解決常見問題和設計挑戰,並提高程式碼的可讀性、可理解性和可維護性。設計原則則為設計模式提供了指導,如單一職責原則、開放封閉原則等。通過應用設計模式和設計原則,開發人員可以構建高質量、可維護和可延伸的軟體系統,避免重複勞動,提高程式碼的可重用性和靈活性。

點選關注,第一時間瞭解華為雲新鮮技術~