通俗易懂設計模式

設計模式 設計 軟件 軟件設計 技術 碼磚雜役 2019-05-28

《設計模式:可複用面向對象軟件的基礎》是一本關於面向對象設計影響廣泛的書,GOF在該書中把常用的設計模式劃分為創建型、結構型、行為型3大類23種。設計模式是為了幫助工程師更快更好地完成軟件設計和實現,避免或減少重複設計,提高系統複用性,使得軟件更優雅、更靈活、更堅固、更具可讀性、可維護性、可擴展性。

通俗易懂設計模式


複用

類繼承對象組合是實現功能複用的兩種常用技術手段,也是軟件對現實世界的抽象和模擬。通過繼承可以擴充基類的功能,也可改寫被複用的基類實現,類繼承是在編譯時刻被靜態確定的,父子類之間是如此緊密的依賴關係,以至於父類的任何變化常導致子類發生變化。對象組合可以獲得被組合對象的引用而在運行時刻動態變化,可以要求對象之間遵守約定接口,而不破壞封裝性。

組合優先於繼承,繼承和組合相輔相成,設計者往往過度使用繼承,但依賴於對象組合的設計往往有更好的複用性。

高內聚低耦合

好的面向對象設計軟件應該是高內聚低耦合的,內聚是從功能角度來度量模塊內的聯繫,一個好的內聚模塊應當恰好做一件事,它描述的是模塊內的功能聯繫。耦合是軟件結構中各模塊之間相互連接的一種度量,耦合強弱取決於模塊間接口的複雜程度。

簡而言之:一個模塊(類、函數)應該專注於一件事情,提供單一功能,避免編寫萬用函數、巨類;模塊之間應減少依賴、降低耦合度,這樣在一個模塊發生變化時,才不會引起廣泛的連鎖反應,從而提高系統的穩定性

具體到實現層面,設計類的時候要最小化接口數量,把承擔內部實現作用的成員函數私有化,模塊之間只通過接口通信,接口應儘量穩定,且數量要少,要符合最小知識原則不要跟陌生人說話,這樣做最終目的是為了隔離

開閉原則

開閉原則是面向對象設計需要遵守的基本規則,對擴展開放,對修改封閉,它主要解決擴展性問題。

良好的軟件設計應該是有有利於擴展的,開閉原則提供了方便擴展、而又不容易引入錯誤的解決思路。

通俗易懂設計模式


示例和問題:畫板的保存與恢復

下面將以一個具體應用為例,給出解決該問題的幾種做法,闡述對象創建型模式的理念,並對比幾種方式的優劣,在解決問題過程中,淺顯易懂的把問題講解清楚。

假設要開發一個畫板程序,畫板上有圓、正方形等幾何圖形,畫板提供保存恢復功能,點擊保存的時候,會將畫板上的圖形保存到磁盤文件。

假設畫板上有一個圓一個正方形,存盤格式如下:

  1. 首先保存一個int32的圖形數量,因為只有一個圓和一個正方形,所以數值為2。
  2. 然後保存圓,圓心位置(float x,y)+半徑(float radius),可以唯一確定一個圓,在x86_32系統上,float佔4字節,所以需要12字節。
  3. 然後保存正方形,左下角位置(float x,y)+邊長(float length)可以唯一確定一個正方形,所以保存正方形也需要12字節。

如果依照上述格式保存到文件之後,恢復畫板圖形的時候,從文件流,首先讀到4字節的圖形數量,數值為2,此時,我們知道,之前畫板上一共有2個圖形,然後程序應該依次讀取2個圖形的存儲數據,但下一步怎麼辦?如何恢復圖形?假設接著讀取12字節,但並不能區分下一個圖形是什麼?也就是說圖形類型信息丟失了,所以,我們需要為每個圖形額外保存類型信息,比如4字節的type(實際上可能一個字節的char就夠了,取決於類型的數量),我們把圓定義為type值等於1,正方形為type值等於2。

如上所述,保存畫板上的圖形(一個圓+一個正方形),存儲的數據格式是:

圖形數量n(4字節)+圖形1存儲數據+圖形2存儲數據+...圖形n存儲數據

畫板上圖形1是圓,那它的存儲格式是:圖形類型(4字節int32 type)+圓心(4字節float x+4字節float y)+半徑(4字節float radius)

圖形2是正方形,格式與此類似。

恢復的時候,首先讀取到圖形數量,然後for i=1,n循環依次恢復每個圖形實例。

恢復每個圖形實例的時候,先讀取圖形類型,然後基於類型做分支,不同類型有不同存儲格式,對應不同的恢復方法,且有不同的繪製方法(Draw)。

其中有一個基於類型做分支的問題,即讀取圖形類型後,如何創建對應圖形的實例?然後再加載對應圖形實例的內部數據(比如圓心位置、半徑等)。

最簡單的if + else if + ...,稍微高級一點的有switch case,以後新加一種圖形類型,便加一個else if或者case,但這樣修改很土,違背了開閉原則裡的對修改封閉條款,當然如果你要抬槓說這樣也挺好,那我無話可說。

對象創建模式

有沒有更高端一點的做法?有,這正是對象創建模式中工廠和原型要解決的問題。

工廠模式

抽象工廠的做法大概是這樣的,工廠(Factory)負責生產(Create)產品,提供一個工廠抽象基類,該抽象基類提供生產的接口,為每一類圖形,提供一個對應的工廠子類,生產對應的產品,比如圓工廠(CircleFactory)生產圓、正方形工廠(SquareFactory)生產正方形,這樣的話,問題分解為2步。

1. 映射,通過圖形類型(type)找到對應的工廠,即type=1,找到CircleFactory實例,type=2,找到SquareFactory示例,這就需要建立圖形類型到對應工廠實例的映射(Map)。

2. 生產,調用對應工廠實例的Create方法,創建相應的產品,比如圓Circle實例或者正方形Square實例。

Create方法很容易定義,對應C++語言,直接new一個相應類型實例返回即可。

Circle* CircleFactory::Create() const { return new Circle; }

Square* SquareFactory::Create() const { return new Square; }

為了滿足抽象工廠的生產(Create)接口定義,需要構建幾何形狀的派生體系,圓Circle和正方形Square都從抽象基類Shape派生,因為C++的虛函數Create,支持子類override版本返回子類對象指針。

至此,問題變成如何建立圖形類型到對應工廠實例的對應關係。

我們可以寫一個工廠管理器(FactoryManager),在工廠管理器裡維護這個對應關係(map很容易做映射),然後創建該工廠管理器的唯一實例(單例)。

這個對應關係(map)我們可以在FactoryManager的構造函數裡構建映射,比如這樣

FactoryManager()
{
map[type1] = new CircleFactory;
map[type2] = new SquareFactory;
}

更好的方法是在Factory裡的構造函數裡通過FactoryManager提供的Register方法往FactoryManager的Map裡添加,因為這樣做,擴展的時候便不需要修改FactoryManager的構造函數,添加類似map[typex] = new XFactory之類的語句,而且FactoryManager所在的文件不需要包含定義工廠子類的所有頭文件,依賴關係上更加合理。

具體而言,需要3步。

1. 構建幾何形狀類的派生體系

通俗易懂設計模式

Shape是抽象基類,故最好定義虛析構函數,然後定義ReadFromFileStream接口用於從文件流讀取數據成員,定義Draw接口用於繪製,Shape類及子類忽視了成員變量的定義。

2. 定義工廠的派生體系

通俗易懂設計模式

抽象工廠定義Create接口,返回Shape指針,具象工廠(CircleFactory等)從抽象工廠派生,實現Create接口,並定義具象工廠的唯一變量,該全局變量的構造函數裡,會將形狀類型到對應工廠對象的對應關係註冊到FactoryManager的map中。

3. 定義工廠管理器

通俗易懂設計模式

工廠管理器維護形狀類型到形狀對應工廠的映射關係,提供Create接口,供Client代碼使用,FactoryManager是一個單例,static Instance方法返回該唯一實例。

用抽象工廠模式改造後,從文件恢復畫板的函數便變成了下面這樣

通俗易懂設計模式

以後要擴展,就不需要修改Restore函數本身了,只需要擴展Shape子類和對應的ShapeFactory子類,並定義一個ShapeFactory子類實例,就可以了,這即是遵循開閉原則的體現。

當然,上面的程序很多缺陷,比如,它沒有做異常處理,FactoryManager的單件的寫法沒有禁用構造函數,以及其他考慮不周詳的地方,但是它不併影響我們解釋工廠模式,我們把關注點放在模式的原理和結構上。

原型模式

原型模式主要基於clone技術,也就是複製,C++程序語言通過拷貝(複製)構造函數支持clone。

它就像日常生活中的複印,比如要複印一份學歷證書,你得先有一份學歷證書,而用於複印的學歷證書,就是原型,需要為每個產品(形狀)創建一個原型對象,然後跟工廠模式類似,它也需要維護形狀類型到原型對象的對應關係,這個對應關係,我們也可以保存在ProtoTypeManager的成員變量(map<形狀類型,形狀原型實例>)裡,通過形狀類型找到原型,然後調用原型的clone方法,基於已有原型對象創建新的對象。

跟工廠模式類似,它也需要定義產品(形狀)的類繼承體系,它要需要維持類型到實例的對應關係。

跟工廠模式不一樣的地方是:

  • 原型模式需要產品提供拷貝構造的能力
  • 原型模式不需要定義工廠類繼承體現
  • 原型模式需要定義產品原型實例,而工廠模式需要定義各種工廠實例

我們也可以在形狀的構造函數裡,把形狀類型到原型對象對應關係添加到map,在ProtoTypeManager裡提供Create接口給client代碼調用。

通俗易懂設計模式

通俗易懂設計模式

原型模式會為每種產品產生至少一個原型,這樣便有許多小對象,而且產品實例的技術,會比應用程序創建的多一個。

單例模式

單例有多種經典寫法,如前文所示,用static local variable是一種慣用手法,另一種是通過單例類靜態成員的指針變量的方式,每次獲取單件實例的時候,先判斷該指針,如果為空,則new出來並賦值給static成員指針變量,然後返回該變量。

單例模式還需要注意以下問題:

  1. 需要解決限制該類型創建多個實例,一般通過私有化構造函數,禁拷貝構造的方式完成。
  2. 在多線程環境下,要解決併發創建的問題,你可以在進程啟動,還沒有創建其他線程的時候,把單例都創建出來,也可以通過多線程同步機制,確保只有一個實例被創建,這會引起一些額外的性能開銷。
  3. 在複雜的軟件環境下,如果有大量不同類型的單件,要處理好,他們之間的構造順序問題。
通俗易懂設計模式


總結

設計模式曾經很火,大家認為它是解決設計問題的好方法,於是便有很多人生搬硬套,然後,便有人反設計模式。

我覺得不能過於追捧設計模式,也不宜一味否認設計模式,它是解決工程問題的一些套路,提供了富有經驗、行之有效的解決方法,瞭解設計模式,對於理解面向對象設計還是很有幫助的。

本篇主要講了創建類設計模式,工廠、原型和單例,另外一種是builder模式,感覺builder價值不大,所以便沒有展開講了。

週末寫技術文章,真的是蠻累的,如果覺得還不錯,請點贊加個關注吧,我會持續更新,提供有價值的技術硬貨,有任何錯誤和疑問,也請評論指正。

通俗易懂設計模式

最後,用一句名言,跟諸君共勉!

相關推薦

推薦中...