標籤:style class blog code http tar
GOF設計模式著作中的23種設計模式可以分成三組:建立型(Creational),結構型(Structural),行為型(Behavioral)。下面來做詳細的剖析。
建立型
建立型模式處理物件建構和引用。他們將對象執行個體的執行個體化責任從客戶代碼中抽象出來,從而讓代碼保持鬆散耦合,將建立複雜物件的責任放在一個地方,這遵循了單一責任原則和分離關注點原則。
下面是“建立型”分組中的模式:
1.Abstract Factory(抽象工廠)模式:提供一個介面來建立一組相關的對象。
2.Factory Method(Factory 方法)模式:支援使用一個類來委託建立有效對象的責任。
3.Builder(產生器)模式:將對象本身的構造分離出來,從而能夠構造對象的不同版本。
4.Prototype(原型)模式:能夠從一個原型執行個體來複製或複製類,而不是建立新執行個體。
5.Singleton(單例)模式:支援一個類只執行個體化一次,並只有一個可用來訪問它的全域訪問點。
結構型
結構型模式處理對象的組合與關係,以滿足大型系統的需要。
下面是“結構型”分組中的模式:
1.Adapter(適配器)模式:使不相容介面的類能夠一起使用。
2.Bridge(橋接)模式:將抽象與其實現分離,允許實現和抽象彼此獨立地改變。
3.Composite(組合)模式:可以像對待對象的單個執行個體那樣來對待一組展示層次結構的對象。
4.Decorator(裝飾)模式:能夠動態封裝一個類並擴充其行為。
5.Facade(門面)模式:提供一個簡單的介面並控制對一組複雜介面和子系統的訪問。
6.Flyweight(享元)模式:提供一種在許多小類之間高效共用資料的方式。
7.Proxy(代理)模式:為一個執行個體化成本很高的更複雜的類提供一個預留位置。
行為型
行為型模式處理對象之間在責任和演算法方面的通訊。這個分組中的模式將複雜行為封裝起來並將其從系統控制流程中抽象出來,這樣就使複雜系統更容易理解和維護。
下面是”行為型“分組中的模式:
1.Chain Of Responsibility(責任鏈)模式:允許將命令動態連結起來處理請求。
2.Command(命令)模式:將一個方法封裝成一個對象,並將該命令的執行與它的調用者分離。
3.Interpreter(解譯器)模式:指定如何執行某種語言中的語句。
4.Iterator(迭代器)模式:提供以形式化的方式來導航集合的方法。
5.Mediator(中介者)模式:定義一個對象,可以讓其他兩個對象進行通訊而不必讓它們知道彼此。
6.Memento(備忘錄)模式:允許將對象恢複到以前的狀態。
7.Observer(觀察者)模式:定義一個或多個類在另一個類發生變化時接到警示。
8.State(狀態)模式:允許對象通過委託給獨立的,可改變的狀態物件來改變自己的行為。
9.Strategy(策略)模式:能夠將演算法封裝到一個類中並在運行時轉換,以改變對象的行為。
10.Template Method(模板方法)模式:定義演算法流程式控制制,但允許子類重寫或實現執行步驟。
11.Vistor(訪問者)模式:能夠在類上執行新的功能而不影響類的結構。
上面介紹了眾多的設計模式及其分組。但是如何來選擇和運用呢?下面有一些需要注意的事項:
1.在不瞭解模式的情況下不能運用他們。
2.在設計的時候,要衡量是否有必要引入設計模式的複雜性。最好能衡量下實現某種模式所需的時間與該模式能夠帶來的效益。謹記KISS原則:保持簡單淺顯。
3.將問題泛化,以更抽象的方式識別正在處理的問題。設計模式是高層次的解決方案,試著把問題抽象,而且不要過於關注具體問題的細節。
4.瞭解具有類似性質的模式以及同組中的其他模式。以前已經使用過某個模式並不意味著在解決問題時它總是正確的模式選擇。
5.封裝變化的部分。瞭解應用程式中什麼可能發生變化。如果知道某個特殊的報價折扣演算法將隨時間發生變化,那麼尋找一種模式來協助您在不影響應用程式其餘部分的情況下改變該演算法。
6.在選擇好設計模式之後,確保在命名解決方案中的參與者時使用該模式的語言及領域語言。例如,如果正在使用原則模式為不同的快遞公司計價提供解決方案,那麼相應地為他們明明,如FedExShippingCostStrategy。通過組合使用模式的公用詞彙表和領域語言,會讓代碼更具可讀性,而且更能夠讓其他具備模式知識的開發人員理解。
就設計模式而言,除了學習之外沒有其他替代方法。對每種設計模式瞭解得越多,在運用他們時就會準備的更好。當遇到一個問題正在尋找解決方案時,掃描一下每種模式的目的,喚起自己的記憶。
一種很好地學習方法就是試著識別.net架構中的模式,比如:Asp.net Cache使用了Singleton模式,在建立新的Guid執行個體時使用了Factory Method模式,.Net 2 xml類使用Factory Method模式,而1.0版並沒有使用。
下面我們以一個快速模式樣本來進行講解,以便於加深映像。
建立一個類庫項目0617.DaemonPattern.Service,然後引用System.web程式集。
首先添加一個Product.cs的空類作為我們的Model:
public class Product { }
然後添加ProductRepository.cs類作為我們的資料存放區倉庫,從這裡我們可以從資料庫擷取資料實體物件:
public class ProductRepository { public IList<Product> GetAllProductsIn(int categoryId) { var products = new List<Product>(); //Database operation to populate products. return products; } }
最後添加一個名稱為ProductService.cs的類,代碼如下:
public class ProductService { public ProductService() { this.productRepository = new ProductRepository(); } private ProductRepository productRepository; public IList<Product> GetAllProductsIn(int categoryId) { IList<Product> products; string storageKey = string.Format("products_in_category_id_{0}", categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } }
從代碼的邏輯,我們可以清楚的看到,ProductService通過ProductRepository倉庫從資料庫擷取資料。
這個類庫帶來的問題有以下幾點:
1.ProductService依賴於ProductRepository類。如果ProductRepository類中的API發生改變,就需要在ProductService類中進行修改。
2.代碼不可測試。如果不讓真正的ProductRepository類串連到真正的資料庫,就不能測試ProductService的方法,因為這兩個類之間存在著緊密耦合。另一個與測試有關的問題是,該代碼依賴於使用Http上下文來緩衝商品。很難測試這種與Http上下文緊密耦合的代碼。
3.被迫使用Http上下文來緩衝。在目前狀態,若使用Velocity或Memcached之類的緩衝儲存提供者,則需要修改ProductService類以及所有其他使用緩衝的類。Verlocity和Memcached都是分布式記憶體對象緩衝系統,可以用來替代Asp.net的預設緩衝機制。
隨意,綜上看來,代碼耦合度過高,不易進行測試,同時也不易進行替換。
既然知道了存在的問題,那麼就讓我們來對其進行重構。
首先,考慮到ProductService類依賴於ProductRepository類的問題。在目前狀態中,ProductService類非常脆弱,如果ProductRepository類的API改變,就需要修改ProductService類。這破壞了分離關注點和單一職責原則。
1.依賴倒置原則(依賴抽象而不要依賴具體)
可以通過依賴倒置原則來解耦ProductService類和ProductRepository類,讓它們都依賴於抽象:介面。
在ProductRepository類上面右擊,選擇“重構”->“提取介面”選項,會自動給我們產生一個IProductRepository.cs類:
public interface IProductRepository { IList<Product> GetAllProductsIn(int categoryId); }
修改現有的ProductRepository類,以實現新建立的介面,代碼如下:
public class ProductRepository : IProductRepository { public IList<Product> GetAllProductsIn(int categoryId) { var products = new List<Product>(); //Database operation to populate products. return products; } }
之後更新ProductService類,以確保它引用的是介面而非具體:
public class ProductService { public ProductService() { this.productRepository = new ProductRepository(); } private IProductRepository productRepository; public IList<Product> GetAllProductsIn(int categoryId) { IList<Product> products; string storageKey = string.Format("products_in_category_id_{0}", categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } }
這樣修改之後,ProductService類現在只依賴於抽象而不是具體的實現,這意味著ProductService類完全不知道任何實現,從而確保它不是那麼容易的被破壞掉,而且代碼在整體上說來對變化更有彈性。
但是,這裡還有個問題,既是ProductService類仍然負責建立具體的實現。而且目前在沒有有效ProductRepository類的情況下不可能測試代碼。所以這裡我們需要引入另一個設計原則來解決這個問題:依賴注入原則。
由於ProductService類仍然與ProductRepository的具體實現綁定在了一起,通過依賴注入原則,我們可以將這一過程移到外部進行,具體方法就是通過該類的構造器將其注入:
public class ProductService { public ProductService(IProductRepository productRepository) { this.productRepository = productRepository; } private IProductRepository productRepository; public IList<Product> GetAllProductsIn(int categoryId) { IList<Product> products; string storageKey = string.Format("products_in_category_id_{0}", categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } }
這樣就可以在測試期間向ProductService類傳遞替代者,從而能夠孤立地測試ProductService類。通過把擷取依賴的責任從ProductService類中移除,能夠確保ProductService類遵循單一職責原則:它現在只關心如何協調從緩衝或資產庫中檢索資料,而不是建立具體的IProductRepository實現。
依賴注入有三種形式:構造器,方法以及屬性。我們這裡只是使用了構造器注入。
當然,現在的代碼看上去基本沒問題了,但是一旦替換緩衝機制的話,將會是一個比較棘手的問題,因為基於Http內容相關的緩衝沒有被封裝,替換其需要對當前類進行修改。這破壞了開放封閉原則:對擴充開放,對修改關閉。
由於Adapter(適配器)模式主要用來將一個類的某個轉換成一個相容的介面,所以在當前的例子中,我們可以將HttpContext緩衝API修改成想要使用的相容API。然後可以使用依賴注入原則,通過一個介面將緩衝API注入到ProductService類。
這裡我們建立一個名為ICacheStorage的新街口,它包含有如下契約:
public interface ICacheStorage { void Remove(string key); void Store(string key, object data); T Retrieve<T>(string key); }
在ProductService類中,我們就可以將其取代基於HttpContext的緩衝執行個體:
public class ProductService { public ProductService(IProductRepository productRepository,ICacheStorage cacheStroage) { this.productRepository = productRepository; this.cacheStroage = cacheStroage; } private IProductRepository productRepository; private ICacheStorage cacheStroage; public IList<Product> GetAllProductsIn(int categoryId) { IList<Product> products; string storageKey = string.Format("products_in_category_id_{0}", categoryId); products = cacheStroage.Retrieve<List<Product>>(storageKey); if (products == null) { products = productRepository.GetAllProductsIn(categoryId); cacheStroage.Store(storageKey, products); } return products; } }
而具體的緩衝類我們可以繼承自ICacheStorage來實現:
public class HttpCacheAdapterStorage:ICacheStorage { public void Remove(string key) { if (HttpContext.Current.Cache[key] != null) HttpContext.Current.Cache.Remove(key); } public void Store(string key, object data) { if (HttpContext.Current.Cache[key] != null) HttpContext.Current.Cache.Remove(key); HttpContext.Current.Cache.Insert(key,data); } public T Retrieve<T>(string key) { if(HttpContext.Current.Cache[key]!=null) return (T)HttpContext.Current.Cache[key]; return default(T); } }
現在再回頭看看,我們解決了開始列舉的種種問題,使得代碼更加容易測試,更易讀,更易懂。
下面是Adapter(適配器)模式的UML圖示:
可以看出,客戶有一個對抽象(Target)的引用。在這裡,該抽象就是ICacheStorage介面。Adapter是Target介面的一個實現,它只是將Operation方法委託給Adaptee類,這裡的Adapter類就是指我們的HttpCacheStorage類,而Adaptee類則是指HttpContext.Current.Cache提供的具體操作方法。
具體的描述如下:
這樣,當我們切換到Memcached,抑或是MS Velocity的時候,只需要建立一個Adapter,讓ProductService類與該緩衝儲存提供者通過公用的ICacheStorage介面互動即可。
從這裡我們知道:
Adapter模式非常簡單,它唯一的作用就是讓具有不相容介面的類能夠在一起工作。
由於Adapter模式並不是唯一能夠協助處理快取資料的模式,下面的章節將會研究Proxy設計模式如何來協助解決緩衝問題的。
在這裡,我們還有最後一個問題沒有解決,就是在當前設計中,為了使用ProductService類,總是不得不為構造器提供ICacheStorage實現,但是如果不希望快取資料呢? 一種做法是提供一個null引用,但是這意味著需要檢查空的ICacheStorage實現從而弄亂代碼,更好的方式則是使用NullObject模式來處理這種特殊情況。
Null Object(Null 物件模式,有時也被稱為特殊情況模式)也是一種極為簡單的模式。當不希望指定或不能指定某個類的有效執行個體而且不希望到處傳遞null引用時,這個模式就有用武之地。Null對象的作用是代替null引用並實現相同的介面但是沒有行為。
如果不希望ProductService類中快取資料,Null Object模式可以派上用場:
public class NullObjectCache:ICacheStorage { public void Remove(string key) { } public void Store(string key, object data) { } public T Retrieve<T>(string key) { return default(T); } }
這樣,當我們請求快取資料的時候,它什麼都不做而且總是向ProductService返回null值,確保不會緩衝任何資料。
最後,總結一下:
三種設計模式分組。
依賴注入原則。
Adapter模式具體應用。
Null Object模式用於處理Null 物件。