大多數開發人員已經聽說過設計模式,
GOF(四人組)模式是最普及的,每個開發人員都有自己的學習方式,我們可以列舉
- 閱讀書籍或雜誌。
- 從網站獲取。
- 從同事那裡學習。
- 參加培訓。
無論選擇哪種方法,我們都可以死記硬背模式並花費數小時來記住它們的 UML 圖,但有時當我們在實際專案中需要使用它們時,就會變得更加困難。
最重要的是要準確地知道模式名稱以及如何在文件中所述的那樣實現它們,但更相關的是每個模式背後的動機,因為正是從動機中我們發明了模式。
為了更好地掌握模式動機,有趣的方式是從實際專案中研究它們。這就是本文的目標,我們將嘗試發現一個大量使用它們的開源專案。
Rigs of Rods 分析
Rigs of Rods(“RoR”)是一款開源的多重模擬遊戲,它使用軟體物理來模擬車輛的運動和變形。該遊戲採用一種稱為 Beam 的特定軟體物理引擎構建,該引擎模擬互連節點的網路(構成底盤和車輪),並能夠模擬可變形物件。透過此引擎,車輛及其負載在施加應力時會彎曲和變形。撞到牆壁或地形可能會永久變形車輛。
在這裡,我們將發現一些使用的 GOF 設計模式,為此我們使用了 CppDepend 來分析 RoR,而 CQLinq 將有助於查詢程式碼庫。
單例(Singleton)
單例是最流行也是最常用的。RoR 使用通用單例來避免為每個單例類重複相同的程式碼,並定義了兩種變體:一種建立例項,另一種分配例項。
讓我們使用以下 CQLinq 查詢搜尋所有 RoR 單例。
1 2
|
from t in Types where t.DeriveFrom("RoRSingletonNoCreation") || t.DeriveFrom("RoRSingleton")
select t
|
動機
以 InputEngine 單例為例,RoR 需要儲存有關鍵盤、滑鼠和操縱桿的資訊,這些資訊在初始化時由 InputEngine 類檢測到,所有類都需要 InputEngine 類的相同資料,並且不需要建立多個例項,因此主要動機是“
建立一個 InputEngine 類的例項”。
為了實現這一點,我們可以將其宣告為全域性變數或將其定義為單例,但是使用單例變得有爭議,並且並非所有架構師和設計師都推薦它,這是一篇
文章討論了單例的爭議。
工廠方法(Factory Method)
關於工廠沒什麼神秘的,它們的目標很簡單:
建立例項,一個包含 CreateInstance 方法的簡單工廠就可以實現這個目標,但是 RoR 在其所有工廠中使用“工廠方法”模式而不是簡單工廠。
動機
為了更好地理解這個模式,讓我們描述一下 RoR 使用這個模式的場景
- RoR 使用圖形引擎
OGRE,並且 OGRE 的一些類需要例項化 ParticleEmitter 類。
- RoR 定義並使用了另一個類 BoxEmitter,它繼承自 ParticleEmitter,並希望 OGRE 使用這個新類作為 ParticleEmitter。
- OGRE 對 RoR 一無所知。
問題是,OGRE 將如何知道如何從 RoR 例項化這個新的 BowEmitter 類並使用它?
“工廠方法”模式的作用就在這裡
OGRE 有一個抽象類 ParticleEmitterFactory,它有一個 CreateEmitter 方法,為了完成它的工作,OGRE 需要一個具體的工廠,RoR 定義了一個新的工廠 BoxEmitterFactory,它繼承自 ParticleEmitterFactory 並重載了 CreateEmitter 方法。
RoR 透過 ParticleSystemManager::addEmitterFactory (ParticleEmitterFactory * factory) 將這個工廠提供給 OGRE。每次 OGRE 需要 ParticleEmitter 的例項時,都會呼叫 BoxEmitterFactory 來建立它。
最重要的動機是
低耦合,實際上 OGRE 對 RoR 一無所知,它可以例項化其中的類。
使用簡單工廠可以隔離例項化邏輯,但使用“工廠方法”更能促進低耦合。
模板方法(Template Method)
模板方法在方法中定義了演算法的骨架,將某些步驟留給子類。模板方法允許子類重新定義演算法的某些步驟,而無需更改演算法的結構。
目的是確保演算法的結構保持不變,同時子類提供部分實現。
讓我們使用 CQLinq 檢測所有使用模板方法模式的類。為此,我們可以搜尋父類中使用了純虛方法並有子類實現這些虛方法的類。
1 2 3
|
from t in Types where t.IsAbstract &&
t.Methods.Where(a=> a.NbLinesOfCode>0 && a.MethodsCalled.Where(b=>b.IsPureVirtual && b.ParentType==t).Count()>0).Count()>0
select t
|
動機
以 IRCWrapper 為例,process 方法實現了模板方法模式,它呼叫了純虛方法 processIRCEvent。
LobbyGui 是一個需要處理 IRC 事件的類,它繼承自 IRCWrapper,處理接收到的 IRC 事件的邏輯集中在 IRCWrapper::process 中,但是對於收到的每個事件,LobbyGui 都必須處理它,因此 process 方法呼叫了被 LobbyGui 類過載的 processIRCEvent。
透過這種模式,我們可以輕鬆地更改演算法的實現而無需更改骨架,它促進了
低耦合,因為客戶端可以僅引用抽象類而不是具體類。
策略(Strategy)
有些常見情況是類僅在行為上有所不同。對於這些情況,將演算法隔離到單獨的類中是一個好主意,以便能夠動態地選擇不同的演算法。
讓我們使用 CQLinq 檢測所有使用策略模式的類。為此,我們可以搜尋抽象類,擁有多個派生類,並且客戶端引用抽象類而不是具體實現。
1 2 3 4
|
from t in Types where t.IsAbstract && t.DirectDerivedTypes.Count()>1 && !t.IsThirdParty
let tt=t.DirectDerivedTypes
from db in tt where db.Methods.Where(a=>a.NbMethodsCallingMe!=0 && !a.IsStatic).Count()==0
select new {db,t}
|
動機
攝像機可以有多種行為:固定、自由、靜態或等距,並且可以動態更改此行為。未來還可以新增其他行為。
CameraManager 使用行為抽象 IBehavior,這裡是所有使用 IBehavior 類的 CameraManager 方法。
正如我們所觀察到的,有一個名為 switchBehavior 的方法用於動態更改行為。
此模式促進
低耦合,CameraManager 不知道具體的行為,並且還促進
高內聚,因為每個特定行為都實現在一個隔離的類中。
狀態
從架構角度來看,狀態(State)模式與策略(Strategy)設計模式相似,因此,對於我們之前搜尋策略模式的 CQLinq 查詢,我們也找到了狀態類。
但是目標不同,策略模式代表使用一個或多個 IStrategy 實現的演算法,並且這些不同的行為之間沒有相關性;但是對於狀態模式,我們從一個狀態傳遞到另一個狀態以實現最終目標,因此不同的狀態之間存在內聚性。
這是所有繼承自抽象類 AppState 的狀態類。
與策略模式一樣,只有抽象類被其他類引用,這裡是所有使用 AppState 的方法。
正如我們所觀察到的,AppStateManager 包含許多管理狀態生命週期的函式。
動機
與策略模式一樣,此模式促進
低耦合,AppState管理器不知道具體的類,並且還促進
高內聚,因為每個處理都隔離在其對應的狀態中。
外觀(Facade)
外觀是一個物件,它為更大的程式碼體(例如類庫)提供簡化的介面。要檢測外觀,最簡單的方法是搜尋使用的外部程式碼。
這裡是 ROR 專案使用的所有名稱空間。
讓我們以 Caelum 為例,並從 RoR 中搜索使用它的類。
1 2
|
from m in Methods where m.IsUsing ("Caelum")
select new { m }
|
只有 SkyManager 直接使用 Caelum 名稱空間,因此它代表了 Caelum 的外觀。
OIS 名稱空間呢?
1 2
|
from m in Methods where m.IsUsing ("OIS")
select new { m }
|
主要是 InputManager 使用 OIS 名稱空間。
動機
如果我們使用外部庫,並且它與我們的程式碼高度耦合,即許多類直接使用該庫,那麼更改外部庫將非常困難。但是,如果使用了外觀,那麼當我們想要更改外部庫時,只會更改其實現。
因此,此模式促進了與外部庫的低耦合。
領域驅動設計方法(Domain Driven Design approach)
領域驅動設計是一種軟體設計方法,基於兩個前提
- 複雜的領域設計應基於模型,並且
- 對於大多數軟體專案,應將重點放在領域和領域邏輯上(而不是用於實現系統的特定技術)。
換句話說,DDD 的核心是模型,開發開始時要做的第一件事就是繪製模型。您建立的模型和設計應該互相塑造。模型應該代表業務知識。
RoR 使用這種方法並將所有資料隔離到結構中,並對此進行檢查,讓我們搜尋所有 ROR 結構。
1 2 3
|
from t in Types where t.IsStructure &&
t.Methods.Where(a=>!a.IsGeneratedByCompiler).Count()==0
select t
|
樹狀圖檢視顯示了受此查詢影響的所有程式碼,藍色矩形代表 CQLinq 查詢的結果。
結論
使用設計模式有很多優點,但如果不理解它們的動機,就很難實現它們,正如我們在本文中所發現的,低耦合和高內聚等動機幾乎存在於所有設計模式中,因此,建議也探索更側重於動機的模式,例如
GRASP,以完成我們對 GOF 的理解。