前段時間,在自己糊里糊涂地寫了一年多的代碼之后,接手了一坨一個同事的代碼。身邊很多人包括我自己都在痛罵那些亂糟糟毫無設計可言的代碼,我不禁開始深思:自己真的比他高明很多嗎?我可以很自信地承認,在代碼風格和單元測試上可以取勝,可是設計模式呢?自己平時開始一個project的時候有認真考慮過設計模式嗎?答案是并沒有,我甚至都數不出有哪些設計模式。于是,我就拿起了這本設計模式黑皮書。
中文版《設計模式:可復用面向對象軟件的基礎》,譯自英文版《Design Patterns: Elements of Reusable Object-Oriented Software》。原書由Erich Gamma, Richard Helm, Ralph Johnson 和John Vlissides合著。這幾位作者常被稱為“Gang of Four”,即GoF。該書列舉了23種主要的設計模式,因此,其他地方經常提到的23種GoF設計模式,就是指本書中提到的這23種設計模式。
把書看完很容易,但是要理解透徹,融匯貫通很難,能夠在實際中靈活地選擇合適的設計模式運用起來就更是難上加難了。所以,我打算按照本書的組織結構(把23種設計模式分成三大類)寫三篇讀書筆記,一來自我總結,二來備忘供以后自己翻閱。與此同時,如果能讓讀者有一定的收獲就更棒了。我覺得本書的前言有句話很對,“第一次閱讀此書時你可能不會完全理解它,但不必著急,我們在起初編寫這本書時也沒有完全理解它們!請記住,這不是一本讀完一遍就可以束之高閣的書。我們希望你在軟件設計過程中反復參閱此書,以獲取設計靈感”。
本節將介紹結構型模式,包括適配器模式、橋接模式、組合模式、裝飾模式、外觀模式、享元模式和代理模式。結構型模式涉及到如何組合類和對象以獲得更大的結構。結構型類模式采用繼承機制來組合接口和實現(一個簡單的例子是多重繼承),如Adapter模式;而結構型對象模式則描述了如何對一些對象進行組合,從而實現新功能的一些方法,如Composite模式。
1. 適配器模式(Adapter)
適配器模式,將一個接口裝換成另一個接口,以符合客戶的預期。Adapter模式使得原本由于接口不兼容而不能一起工作的那些類可以一起工作。
動機:兩個已有的接口不兼容,我們需要在不改變這兩個接口現有功能的前提下,使它們能夠協同工作。
結構圖:
代碼示例:
下面是一個使用Adapter的簡單例子,我們原本的接口是ITurkey
,但是用戶希望看到的是IDuck
,所以就在兩者之間增加了TurkeyAdapter
,使得原本的ITurkey
也能像IDuck一樣使用。
public interface IDuck
{
void Quack();
void Fly();
}
public interface ITurkey
{
void Gobble();
void Fly();
}
public class TurkeyAdapter : IDuck
{
private ITurkey turkey;
public TurkeyAdapter(ITurkey turkey)
{
this.turkey = turkey;
}
public void Quack()
{
this.turkey.Gobble;
}
public void Fly()
{
foreach(int i in Enumerable.Range(0, 5))
{
this.turkey.Fly();
}
}
}
一個適配器只能夠封裝一個類嗎?
適配器模式的工作是將一個接口轉換成另一個。雖然大多數的適配器模式所采取的例子都是讓一個適配器包裝一個被適配者,但我們都知道這個世界其實復雜多了,所以你可能遇到一些狀況,需要讓一個適配器包裝多個被適配者。這涉及另一個模式,被稱為外觀模式Facade。
萬一我們想在增加對新接口的適配的同時保持舊的接口怎么辦呢?
我們可以創建一個雙向的適配器,支持兩邊的接口。想創建一個雙向的適配器,就必須實現所涉及的兩個接口,這樣,這個適配器可以當做舊的接口,或者當做新的接口使用。
2. 橋接模式(Bridge)
橋接模式,是將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
動機:一個經典的應用場景是,利用多層繼承實現跨平臺,如考慮在一個用戶界面工具箱中,一個可移植的Window抽象部分的實現。所以我們先定義一個抽象的接口Window,然后為了實現在不同系統中的應用,分別定義了Window的兩個子類XWindow和PMWindow。當增加一個子類IconWindow,為了使它支持兩個系統平臺,我們必須為IconWindow實現兩個新類XIconWindow和PMIconWindow。
橋接模式用一種巧妙的方法處理多層繼承存在的問題,用抽象關聯取代了傳統的多層繼承,將類之間的靜態繼承關系轉換成動態的對象組合關系,使得系統更靈活更容易擴展,同時有效控制系統中類的個數。
結構圖:
代碼示例:
下面是使用Bridge模式的一個簡單的例子,其中我們分別定義了兩個接口IBridge
和IAbstractBridge
,前者用來定義IBridge
接口的具體功能,后者則提供了實現Bridge功能的具體調用方法。
//Helps in providing truly decoupled architecture
public interface IBridge
{
void Func1();
void Func2();
}
public class Bridge1 : IBridge
{
public void Func1()
{
Console.WriteLine("Func1 in Bridge1");
}
public void Func2()
{
Console.WriteLine("Func2 in Bridge1");
}
}
public class Bridge2 : IBridge
{
public void Func1()
{
Console.WriteLine("Func1 in Bridge2");
}
public void Func2()
{
Console.WriteLine("Func2 in Bridge2");
}
}
public interface IAbstractBridge
{
void CallFunc1();
void CallFunc2();
}
public class AbstractBridge : IAbstractBridge
{
public IBridge bridge;
public AbstractBridge(IBridge bridge)
{
this.bridge = bridge;
}
public void CallFunc1()
{
this.bridge.Func1();
}
public void CallFunc2()
{
this.bridge.Func2();
}
}
適用情況:
(1) 當你不希望抽象部分與實現部分之間有一個固定的綁定關系。比如,若需要運行時刻可以對實現部分進行切換。
(2) 類的抽象及它的實現都應該可以通過子類的方法進行擴充。
(3) 對一個抽象的實現部分的修改不會影響客戶。
(4) 當想在多個對象間共享實現(可能使用引用計數),但同時要求客戶并不知道這一點。
3. 組合模式(Composite)
組合模式,是將對象組合成樹形結構以表示“整體-部分”的層次結構,從而使得用戶對單個對象和組合對象的使用具有一致性。
動機:對于樹形結構,由于容器對象和葉子對象在功能上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數情況下我們希望一致地處理它們,因為對于這些對象的區別對待將會使得程序非常復雜。
結構圖
代碼示例:
下面是組合模式的簡單示例,我們讓Leaf
和Normal
都實現自IComposite
,即用IComposite
抽象它們都具有的功能來實現對它們操作的統一性。
public interface IComposite
{
void Operation();
}
public class LeafComposite : IComposite
{
public void Operation()
{
Console.WriteLine("Operating leaf");
}
}
public class NormalComposite : IComposite
{
public void Operation()
{
Console.WriteLine("Operating composite");
}
public void Add(IComposite composite)
{
}
public void Remove(IComposite composite)
{
}
public IComposite GetChild(int i)
{
throw new NotImplementedException();
}
}
適用情況:
(1) 你想表示對象的部分-整體層次結構;
(2) 你希望用戶忽略組合對象與單個對象的不同,用戶將統一地使用組合結構中的所有對象。
4. 裝飾模式(Decorator)
裝飾模式,可以動態地給一個對象添加一些額外的職責。繼承也可以實現增加功能,但是裝飾模式會比生成子類更加靈活。
動機:有時我們希望給某個對象而不是整個類添加一些功能,例如,一個圖形用戶界面工具箱允許你對任意一個用戶界面組件添加一些特性,如邊框。我們可以采用繼承機制實現一個帶有邊框特性的組件子類,但是這樣邊框的選擇是靜態的,用戶不能控制對組件加邊框的方式和時機。一種較為靈活的方式是將組件嵌入另一個對象中,由這個對象添加邊框。我們稱這個嵌入的對象為裝飾。
結構圖:
代碼示例:
下面是裝飾模式的簡單示例。我們原本可能有一系列不同的類實現IShape
,現在我們需要對這些圖形添加顏色。如果直接在IShape
里面添加顏色,則所有實現它的類都需要更改,因此我們使用裝飾模式定義ColoredShape
,其中包含一個IShape
對象的引用。
public class IShape
{
string GetName();
}
public class Circle : IShape
{
private double radius;
public string GetName()
{
return "A circle";
}
}
public ColoredShape : IShape
{
private string color;
public IShape shape;
public string GetName()
{
return shape.GetName() + "which is in " + color;
}
}
適用情況:
(1) 在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責;
(2) 處理那些可以撤銷的職責;
(3) 當不能采用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,為支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用于生成子類。
5. 外觀模式(Facade)
外觀模式,為子系統中的一組接口提供一個一致的界面。外觀模式定義一個高層接口,這個接口使得這一子系統更加容易使用。
動機:當一個系統較為龐雜時,一個常見的設計是將它劃分成若干個相互依賴較小的子系統,引入外觀對象為子系統提供一個單一而簡單的界面就是方法之一。例如有一個編程環境,它允許應用程序訪問它的編譯子系統。這個編譯子系統包含了多個不同的子類,有些特殊應用程序需要直接訪問這些類,但是大多數用戶只是希望編譯一些代碼。所以,為了提供一個高層的接口并且對客戶屏蔽這些類,編譯子系統還包括一個Compiler類,這個類定義了一個編譯器功能的統一接口。Compiler類就是一個外觀,它給用戶提供了一個單一而簡單的編譯子系統接口。
結構圖:
代碼示例:
下面是外觀模式的簡單示例。在我們的汽車制造系統下有很多小系統,然而我們使用時一般都是需要一臺完整的車。所以我們提供了一個CarFacade
類屏蔽了CreateCompeleteCar
方法的細節。
public class CarModel
{
public void SetModel()
{
}
}
public class CarEngine()
{
public void SetEngine()
{
}
}
public class CarBody
{
public void SetBody()
{
}
}
public class CarAccessories
{
publiv void SetAccessories()
{
}
}
public class CarFacade
{
private readonly CarModel model;
private readonly CarEngine engine;
private readonly CarBody body;
private readonly CarAccessories accessories;
public CarFacade()
{
model = new CarModel();
engine = new CarEngine();
body = new CarBody();
accessories = new CarAccessories();
}
public void CreateCompeleteCar()
{
model.SetModel();
engine.SetEngine();
body.SetBody();
accessories.SetAccessories();
}
}
適用情況:
(1) 當你要為一個復雜子系統提供一個簡單接口時。
(2) 客戶程序與抽象類的實現部分之間存在著很大的依賴性。引入Fa?ade將這個子系統與客戶以及其他的子系統分離,可以提高子系統的獨立性和可移植性。
(3) 當你需要構建一個層次結構的子系統時,使用Fa?ade模式定義子系統中每層的入口點。如果子系統之間是相互依賴的,你可以讓它們僅通過Fa?ade進行通訊,從而簡化了它們之間的依賴關系。
6. 享元模式(Flyweight)
享元模式,運用共享技術有效地支持大量細粒度的對象。
動機:例如大多數文檔編輯器的實現都有文本格式化和編輯功能,這些功能在一定程序上是模塊化的。面向對象的文檔編輯器通常使用對象來表示嵌入的成分,如表格和圖形。然而通常并不是對每個字符都用一個對象來表示,因為這樣會耗費大量的內存產生難以接受的運行開銷。享元模式描述了如何共享對象,使得可以細粒度地使用它們而無需高昂的代價。
結構圖:
代碼示例:
下面是享元模式的簡單示例。假設我們的系統中記錄了一些員工信息,其中員工信息中又包含他所在的公司的一些信息。因此我們為這個公司創建了一個靜態的共享對象FlyweightPointer.Company
,這樣所有的員工就不需要再單獨存儲公司相關的信息。
public class Flyweight
{
public string CompanyName {get; set;}
public string CompanyLocation {get; set;}
public string CompanyWebsite {get; set;}
public byte[] CompanyLogo {get; set;}
}
public static class FlyweightPointer
{
public static readonly Flyweight Company = new Flyweight
{
CompanyName = "CompanyName",
CompanyLocation = "CompanyLocation",
CompanyWebsite = "www.website.com"
// Load CompanyLogo
};
}
public class Employee
{
public string Name {get; set;}
public string Company
{
get
{
return FlyweightPointer.Company.CompanyName;
}
}
}
適用情況:
Flyweight模式的有效性很大程度上取決于如何使用它以及在何處使用它,當以下情況都成立時使用Flyweight模式:
(1) 一個應用程序使用了大量的對象;
(2) 完全由于使用大量的對象造成很大的存儲開銷;
(3) 對象的大多數狀態都可變為外部狀態;
(4) 如果刪除對象的外部狀態,那么可以用相對較小的共享對象取代很多組對象;
(5) 應用程序不依賴于對象標識。
7. 代理模式(Proxy)
代理模式,為其他對象提供一種代理以控制對這個對象的訪問。
動機:對一個對象進行訪問控制的一個原因是為了只有在我們確實需要這個對象時才對它進行創建和初始化。考慮一個可以在文檔中嵌入圖形對象的文檔編輯器,有些圖形對象的創建開銷可能會很大,而我們要求必須迅速打開文檔,即打開文檔的時候應避免一次性創建所有開銷很大的對象。問題的解決方案是使用另一個對象,即圖像Proxy,替代那個真正的圖像,在需要的時候Proxy負責實例化這個圖像對象。
結構圖:
代碼示例:
下面是代理模式的簡單示例。我們需要操作的真實的實例是Car
,但是為了對操作Car
附加一些限制,我們增加了ProxyCar
來做一些額外的檢查,只有當滿足一定的條件后,ProxyCar
才會將真實的request轉發給Car
,即this.realCar.DriveCar()
。
public interface ICar
{
void DriveCar();
}
// Real Object
public class Car : ICar
{
public void DriveCar()
{
Console.WriteLine("Car has been driven!");
}
}
// Proxy Object
public class ProxyCar : ICar
{
private Driver driver;
private ICar realCar;
public ProxyCar(Driver driver)
{
this.driver = driver;
this.realCar = new Car();
}
public void DriveCar()
{
if(driver.Age <= 16)
{
Console.WriteLine("Sorry, the driver is too young to drive.");
}
else
{
this.realCar.DriveCar();
}
}
}
public class Driver
{
public int Age {get; private set;}
public Driver(int age)
{
this.Age = age;
}
}
適用情況:
(1)遠程代理為一個對象在不同的地址空間提供局部代表。
(2)虛代理根據需要創建開銷很大的對象。
(3)保護代理控制對原始對象的訪問,保護代理用于對象應該有不同的訪問權限的時候。
(4)智能指針取代了簡單的指針,它在訪問對象時執行了一些附加操作。
8. 小結
結構型模式的目標都是如何組合類和對象以獲得更大的結構,以上這七種模式存在一定的相似性,但是也有各己的優缺點和適用情況。下面是一些相似模式的比較:
(1)Adapter vs. Bridge
它們都是給另一對象提供一定程度的間接性,因而有利于系統的靈活性。它們都涉及到從自身以外的一個接口向這個對象轉發請求。
Adapter模式主要是為了解決兩個已有接口之間不匹配的問題。它不需要考慮這些接口如何實現,也不考慮它們各自將如何演化。
Bridge模式則對抽象接口與它的實現部分進行橋接。雖然這一模式允許你修改實現它的類,它仍然為用戶提供了一個穩定的接口。Bridge模式也會在系統演化時適應新的實現。Bridge模式一般用于設計階段,因為它的使用者必須事先知道,一個抽象將有多個實現部分并且抽象和實現兩者是獨立演化的。
(2)Adapter vs. Fa?ade
前者是兩個接口的適配器,后者定義了一個新的接口。
(3)Adapter vs. Decorator
前者是使兩個已有的接口能夠協同工作,后者是在現有接口缺乏某種功能而在不生成子類的前提下給對象添加職責。
(4)Composite vs. Decorator
前者旨在構造類,使多個相關的對象能夠以統一的方式處理,而多重對象可以被當做一個對象來處理。后者旨在使你能夠不需要生成子類即可給對象添加職責。這就避免了靜態實現所有的功能組合從而導致子類急劇增加。
(5)Decorator vs. Proxy
兩種模式都是描述了怎樣為對象提供一定程度上的間接引用,Proxy和Decorator對象的實現部分都保留了一個指向另一個對象的指針,它們向這個對象發送請求。
像Decorator模式一樣,Proxy模式構成一個對象并為用戶提供一致的接口。但是不同的是,Proxy模式不能動態地添加或者分離性質,它也不是為遞歸組合而設計的。它的目的是,當直接訪問一個實體不方便或者不符合需求時,為這個實體提供一個替代者。
Proxy模式中,實體定義了關鍵功能,而Proxy提供或者拒絕對它的訪問。在Decorator模式中,組件僅提供了部分功能,而一個或多個Decorator負責完成其他功能。Decorator模式適用于編譯時不能確定對象的全部功能的情況。
參考文獻:
《設計模式 可復用面向對象軟件的基礎》
《Head first 設計模式》
設計模式之結構型模式