文章出處

感謝大家的支持,本系列第2篇在此推出。朋友們還可關注我的新浪微博“同濟王陽”,上面會定期分享一些本人所關注的技術熱點資訊、博文、視頻、國外教程等內容,相信我們品味還是比較一致的哦!

1     概述

1.1    設計模式概述

設計模式屬于面向對象軟件設計方法論的范疇。

我們知道,面向對象的概念是在過程化程序設計出現軟件危機的時代背景中被提出的。在那時,這屬于一個嶄新的事物。在80年代末、90年代初,不僅我們現在認為的面向對象設計方法的集大成者——.NET平臺的BCL類庫和Java平臺的JDK類庫連影子都找不到,甚至隨便找一個面向對象的代碼都很困難。那時的軟件開發者不了解面向對象的設計方法,即使純粹為沾上面向對象而寫一些代碼,其設計之拙劣也很難體現出面向對象的優勢,和今天的面向對象新手情況類似。我們可以體會誕生于那時的MFC等今天仍在廣泛使用的類庫與后來的BCL類庫在設計思想上的差異。毫無疑問,后者的設計是更加精良的。造成這種進化的原因,就是關于設計模式的廣泛討論和普遍接受。

四人組(GoF)并不是首先提出模式和進行模式研究的人,然而他們在1994年推出的著作Design Patterns: Elements of Reusable Object-Oriented Software(中文名:《設計模式:可復用面向對象軟件的基礎》)被奉為設計模式領域的經典。書中歸納總結了23種設計模式,告訴人們如何設計出靈活的面向對象軟件架構,發揮面向對象的優勢,實現代碼可復用。“可復用”的終極形式是API。今天我們能夠用上Boost、BCL、JDK甚至Cocoa,設計模式功不可沒。

1.2    參考資料

本教程主要綜合以下資料中的觀點和我公司開發實踐寫成:

  • 《設計模式之禪》,秦小波,機械工業出版社
  • 《.NET與設計模式》,甄鐳,電子工業出版社
  •  Design Patterns: Elements of Reusable Object-Oriented Software, GoF(四人組),中文名:《設計模式:可復用面向對象軟件的基礎》
  •  Design Patterns Explained,中文名:《設計模式解析》
  •  C# 3.0 Design Patterns

     

這些資料可以用作延伸閱讀材料。

 

2     面向對象設計的六大原則

在面向對象程序設計領域有一個SOLID原則,是由羅伯特·C·馬丁在21世紀早期引入的指代面向對象編程和面向對象設計的五個基本原則(單一功能、開閉原則、里氏替換、接口隔離以及依賴倒置)的記憶術首字母縮略字,合起來正好是“堅固的”的意思。當這些原則被一起應用時,它們使得一個程序員開發一個容易進行軟件維護和擴展的系統變得更加可能。另外還有一個“迪米特法則”,與SOLID合稱“六大原則”。

2.1    單一職責原則(Single Responsibility Principle, SRP)

There should never be more than one reason for a class to change.

不應有一個以上的原因引起類的變化。

要求一個類只有一個職責。基于這個原則,現實開發中的許多類需要拆分,這與“小類小函數”的說法是一致的。

小函數的好處有:

  • 合并重復代碼,便于維護
  • 增加函數層級,便于調試

小類的好處可以比照小函數。試想一種極端情況:新手初學C#往往不知道怎樣使用類,拉了一堆控件,紛紛雙擊添加事件處理器,整個程序寫了幾千行,都在Form1.cs一個文件里,到最后根本找不到每個函數的位置,這和面向過程的編程沒什么兩樣。 

在Java和ActionScript中要求每個類放在單獨文件中。在C#和VB.NET中雖然沒有這種要求,但是不妨作為一種好的代碼風格執行。項目較大時用文件夾組織代碼文件。 

C++中的多重繼承與本原則是矛盾的。因此,要避免使用。

Scala中的trait怎么辦呢?trait一詞意為“特質”,不妨想象兩個類的職責都是“瓷器活”,但其中一個有“金剛鉆”特質。顯然,不管是金剛鉆還是什么鉆,只要用來干瓷器活,改變不了職責的本質,即恰當使用trait不違反單一職責原則。但是,如果變成“賣金剛鉆”,就另當別論了,此時trait的使用就不合適了。

2.2    開放-封閉原則(Open Closed Principle, OCP)

Software entities like classes, modules and functions should be opened for extension but closed for modifications.

軟件實體應該對擴展開放,對修改關閉。

開閉原則是面向對象編程的基礎性原則。之前的5個原則都是開閉原則的具體形態。開閉原則可以提高代碼的復用性和可維護性。

有的人會提出疑問,說只要接口不變,一個模塊的內部實現怎樣修改又有什么關系呢?

問題的關鍵在于面向對象編程的局限性。以函數為例,一個函數往往并不是接收參數、返回結果那么簡單,即使在面向對象編程中,我們也往往會利用函數的“副作用”——事實上,所有的void函數不都是這樣嗎?盡管.NET等現代開發規范建議盡量把void函數改為返回新值的函數,并且建議不使用輸出型參數,但是在面向對象范式中這并不總是可行。在這種情況下,僅僅接口不變是不能保證系統正常工作的。

在更多情況下,我們拿到了一個第三方庫,需要用到我們的系統中。即使這個庫是開源的,我們也不應去修改其源碼。應當在庫的基礎上通過繼承、設計模式等手段進行擴展。這樣,對于將來庫的升級更新,我們的系統才能輕松應對。

2.3    里氏替換原則(Liskov Substitution Principle, LSP)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基類的地方必須能透明地使用其子類的對象。

初學者往往搞不清基類和派生類的關系問題。他們會問:基類和派生類到底是誰包含誰?

這個問題的前提是錯誤的。基類和派生類之間不是包含與被包含的關系,二者是共性與個性,一般與特殊的關系。

如果一定要回答前述問題,我只能從表面形式上給出一個描述。從實例數量上看,基類比子類對象多,因為基類對象數是所有子類對象數的總和。從復雜性上看,子類比基類有更多的屬性與功能,因為子類首先是基類,必須包含基類已有的全部特性。

在C#等現代面向對象語言中,基類變量可以指向子類實例,反之則不行。當且僅當基類變量指向子類實例時,可以通過引用轉換將基類變量賦值給子類變量。“子類”又稱“運行時類型”。在面向對象編程中通過使用編譯時基類類型指向運行時子類類型來實現多態。

在.NET和Java等現代面向對象平臺中,使用“接口”類型來表示一個抽象基類的接口形式。從一個接口類型繼承,稱為“實現接口”。

復雜類型(這里指利用泛型由已知類型組合出來的新類型,如List<int>,Func<double, double>)的繼承關系涉及到類型系統中協變(Covariant)與逆變(Contravariant)的概念。初學者理解比較困難,只需記住一句話即可:子類的輸入類型范圍可放大,輸出類型范圍可縮小。讀者可體會這句話為什么是符合里氏替換原則的。在.NET 4.0引入協變和逆變后,若Dog是Animal的子類,則IEnumerable<Dog>是IEnumerable<Animal>的子類,而Action<Dog>是Action<Animal>的基類。(注:請不要以object和int代替Animal和Dog,通不過語法檢查)

派生類重寫基類方法時,也遵從這句話,輸入類型范圍可以放大(使用基類,逆變),輸出類型范圍可以縮小(使用子類,協變)。

2.4    接口隔離原則(Interface Segregation Principle, ISP)

The dependency of one class to another one should depend on the smallest possible interface.

類間的依賴關系應該建立在最小的接口上。

另一種表述是:客戶端不應依賴它不需要的接口。

這個原則針對的是這樣一種情況:定義了一個很大的接口,包含10個以上方法。問題是,客戶端調用接口完成一項功能,需要同時調用這么多方法嗎?

事實上,這樣的大接口往往可以拆分成若干小接口,這與單一職責原則呼應。類在實現接口時,可以從這些接口中選擇需要的部分。按照.NET開發規范建議,一個接口的成員數量不宜超過3個。

2.5    依賴倒置原則(Dependence Inversion Principle, DIP)

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。

另外一種通俗的表述為:針對接口編程,不要針對實現編程。

這個原則是6大原則里最難實現的一個。它要求:

  • 每個類都要有接口或抽象類
  • 變量的表面類型盡量是接口或抽象類
  • 任何類都不應該從具體類派生
  • 盡量不要重寫基類的方法

按照這個原則編寫的程序毫無疑問具有很大靈活性,但是在小團隊中會對工作量和程序員本身的水平提出巨大挑戰。在大型團隊中,有專門的架構師來制定系統接口作為并行開發的各代碼組之間的契約,這樣高層組無需等待低層組完成實現,所有代碼組同時針對契約進行開發。

然而,此原則所蘊含的思想是值得學習的。即使在小團隊開發中,特定的場景也可以使用。

2.6    最少知識原則(Least Knowledge Principle, LKP)

也稱作“迪米特法則”(Law of Demeter)。

Objects should have least possible knowledge about each other.

一個對象應該對其他對象有最少的了解。

  

3     設計模式選講

3.1    設計模式總覽

23種設計模式按照功能特點可以分為創建型模式、結構型模式、行為型模式3類,而按照作用對象又包括類模式和對象模式2種,見下表。

 

設計模式的中心思想是解耦(Decouple)。解耦就是解除耦合,具體來說是使得軟件系統的各個模塊實現高內聚、低耦合。這樣,在系統維護時,著眼點只需落在各個模塊上,處理一個模塊時,對其他模塊造成的影響盡量小,從而降低維護的難度。

以下我們選講一些常見的模式。在此,我們不打算給大家看每個模式的代碼。我們總是以四人組原著中的模式定義(中英文)作為開頭,隨后給出模式的類圖(類圖來自O’Reilly的C# 3.0 Design Patterns一書,可參看),接著是一段筆者對此模式的理解和感悟。這樣安排的理由很簡單,博文不是教科書或者工具書,無法代替系統的學習和精確的查閱,學習和查閱的功能應該轉去教科書和工具書。博文的目的就是提供一種印象,就像同事之間隨意聊天間獲取資訊。每個人都會獲取獨一無二的印象,并在之后選擇性地進行深入了解。

 

3.2    單例模式(Singleton)

Ensure a class has only one instance, and provide a global point of access to it.

確保某一個類只有一個實例,自行實例化并向整個系統提供這個實例。

單例模式比較簡單,也很經典,就讓我們來看一下代碼。

public class SingletonClass
{
    public static SingletonClass _singleton = new SingletonClass(); 

    // 限制從外部創建對象
    private SingletonClass()
    {
    }

    // 全局訪問結點
    public static SingletonClass Singleton
    {
        get
        {
            return _singleton;
        }
    }
}

單例模式用于系統中獨一無二事物的抽象。例如,三維程序中的場景管理器,程序中的一個菜單面板。這些內容如果重復創建,首先從邏輯上說不通。即使你用其他手段解決了邏輯問題,比如你保證只有一個變量,但是每次用的時候為了刷新去重新創建,也是一種浪費資源的表現。

在.NET框架的許多API中,經常把對“單例”的訪問進一步簡化為靜態訪問。 

 

3.3    工廠模式(Factory)

 

工廠模式幾乎是最如雷貫耳的設計模式,然而理解它并不是那么容易。工廠模式是簡單工廠、工廠方法、抽象工廠3個設計模式的非正式總稱,其中后兩者屬于23種設計模式,而簡單工廠不是。

工廠模式的目的是避免直接實例化一個類,從而降低功能邏輯代碼對實現的耦合。這體現了“依賴倒置原則”。

工廠模式的應用實例常見于數據庫語境中。數據庫存在多種提供者,開發人員不希望自己的業務代碼依賴具體的數據庫,如SQL Server,Oracle,MySQL等,但是代碼中創建的對象必然是實實在在的某種確定數據庫的對象。工廠模式就用在這里。

.NET中有很多名稱叫XxxFactory的類,名稱暗示其使用了工廠模式。

 

3.4    建造者模式(Builder)

Separate the construction of a complex object from its representation so that the same construction process can create different representation.

將一個復雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。

每個.NET程序員都應當熟悉這個模式,回想一下StringBuilder這個類,正如名稱所暗示的,其就是建造者模式的一個絕佳范例。String類是一個簡單而昂貴的類,為了解決昂貴的初始化問題,引入建造者模式,用復雜但高效的內部算法完成創建字符串,再轉化為String類。

另一個典型的適用建造者模式的情景是三維網格。Mesh的數據結構很簡單,就是一個點集和一個面集。然而,構造這個Mesh的過程可以非常復雜,也有多種構建方法可以選擇,各種方法之間還可以依次疊加組合。有了建造者模式,通過另一個MeshBuilder類來表示Mesh的創建過程,創建好后每次調用ToMesh方法就可以得到一個Mesh。

 

3.5    模板方法模式(Template Method)

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

定義一個操作中的算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。

模板方法模式的語義類似于“自頂向下”“測試驅動開發(TDD)”,它們的關注點都在上層代碼,要求在編寫上層代碼時,基于接口編程;而底層代碼會根據情況有不同的接口實現。作為設計模式,模板方法要求按照繼承的方法來定義底層代碼。

各平臺、各框架中的回調機制就是典型的模板方法模式。例如在Android開發中,我們在子類中重寫各種回調函數,實際上就是在Android給出的大的代碼模板中加入我們自己的代碼。代碼模板已經決定了各回調代碼的執行順序、協作關系。

而在.NET中,實際上對模板方法模式有了發展和簡化。.NET引入了事件的語法(而不僅僅是概念),這使得不需要為每一種具體方法定義一個子類,只需向模板方法(事件)掛接委托就可以了。從而大大減少了類型膨脹。

 

3.6    原型模式(Prototype)

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新對象。

四人組提出原型模式的原意是使得C++能夠動態地創建對象,而不需要知道對象類型的具體細節。近年來有的書上將原型模式解釋為通過內存流復制對象從而獲得高性能。

我們知道C++中拷貝一個對象是很方便的,而在Java和C#中卻幾乎是一個不可能的任務,除非每個成員字段的類型都實現了克隆接口,或者每個成員字段都標記為Serializable。顯然只有后者通過序列化技術拷貝對象才具有高性能。

原型模式的一個應用是創建構建過程復雜的對象。例如StringBuilder的例子,當我們把幾十萬個字符串拼接在一起并轉成String后,如果想復制一個,就沒必要再用一次StringBuilder了吧!直接String.Copy()就行了。再例如,我們有一個三維對象,現在需要創建它的鏡像,并希望鏡像后可以獨立編輯。由于源對象是如何建立的已經無從得知,只能用克隆的辦法。

說到原型就不能不提“基于原型的面向對象語言”JavaScript。該語言通過原型鏈來提高創建對象的效率。

反射(Reflection)是面向對象領域的一個重要技術,它使程序有了運行時操作自身的能力,包括獲取每個類型的詳細信息、動態創建對象、運行時綁定方法等。在CLR之前,JVM已經提供了反射功能,為CLR的設計提供了很好的借鑒。反射支持根據Type動態創建對象,據此有人提出了“反射工廠”模式。

 

3.7    中介者模式(Mediator)

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

用一個中介對象封裝一系列的對象交互。中介者模式使各對象不需要顯式地相互引用,從而使其耦合松散,而且可以獨立地改變它們之間的交互。

中介者模式最典型的應用是在界面框架(如Windows Forms和WPF)中。窗體類就是這個中介者。窗體中包含了大量的控件。界面邏輯的本質就是用一個控件去操作另一個控件。如果我們要對每一對可能發生關系的控件都分別編寫代碼,程序中將多出很多類,并且相互關系復雜到一種境界。而在界面框架實際中,我們的全部代碼均寫在繼承的窗體類這個中介者中。任何控件想要改變其他控件,必須引發一個事件。我們知道,事件本質上就是委托變量,引發事件就是讓變量里的函數執行。而控件的事件里保存了在窗體類中定義的事件處理函數的引用,引發事件相當于控件調用了中介者的成員函數。

中介者模式減少了類間的依賴,把原有一對多的關系變成了一對一的關系,降低了耦合

前面講過初學者的Form1.cs往往很大的例子,這說明,中介者模式使得中介者類變得臃腫、復雜

 

委托是.NET平臺上的一大法寶。我們知道C#最初從Java獲得靈感,然而CLR上的委托機制讓C#如魚得水,愈發強大,發展出了優雅靈活的lambda語法,到今天對Java 8進行了反哺。委托被解釋為“類型安全的函數指針”。然而與C++函數指針相比委托的強大之處還體現在:

  • 可動態掛接、斷開多個函數。
  • 使得函數作為頭等值(first-class value)有了可能。
  • 可直接異步執行,多線程編程中應用廣泛。
  • 支持函數中定義函數、實現閉包。
  • 屬于平臺(CLR)功能而不是語言功能或者類庫功能。

 

3.8    命令模式(Command)

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

將一個請求封裝成一個對象,從而讓你使用不同的請求把客戶端參數化,對請求排隊或者記錄請求日志,可以提供命令的撤銷和恢復功能。

 

WPF的命令機制實現了命令模式。

public interface ICommand
{
    // Events
    event EventHandler CanExecuteChanged; 

    // Methods
    bool CanExecute(object parameter);
    void Execute(object parameter);
}

從中可以看到,命令模式使得調用方和執行方解耦

某軟件API中強制使用WPF的ICommand接口。這使得習慣于直接向按鈕添加事件處理器的程序員感到不適應——原本寫一個函數的事情現在需要寫一個類。由此可以看出,命令模式使得類型數迅速膨脹

但是我們可以實現一些通用的命令類。例如我實現了這樣一個命令類,用于執行一個函數,代碼如下。

public class MyCommand : System.Windows.Input.ICommand
{
    public bool CanExecute(object parameter)
    {
        return true;
    } 

    public event EventHandler CanExecuteChanged; 

    public void Execute(object parameter)
    {
        Executable(parameter);
    }

    public Action<object> Executable = x => { };
}

在調用代碼中這樣使用:

MyCommand cmd = new MyCommand { Executable = x => MessageBox.Show("Hello") };
button.CommandHandler = cmd;

這實際上是模板方法模式的思想。結合模板方法模式可以減少Command子類膨脹的問題。

3.9    責任鏈模式(Chain of Responsibility)

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handle it.

使多個對象都有機會處理請求,從而避免了請求發送者和接受者的耦合關系。將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有對象處理它為止。

責任鏈模式的典型例子是WPF中的路由事件(Routed Event)機制。路由事件分為冒泡(Bubbling)事件和隧道(Tunneling)事件兩種。任何一個控件引發了路由事件,都會沿著可視樹(Visual Tree)進行傳遞,冒泡事件是從引發者向上傳到根元素,隧道事件是從根元素向下傳到引發者。途經每一個控件結點都會引發相應事件,除非中途在某結點標記事件已經處理。

3.10    裝飾模式(Decorator)

Attach additional responsibilities to an object dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.

動態地給一個對象添加額外職責,并保持接口不變。裝飾模式在擴展功能上比繼承更靈活。

 

繼承的基類不可能到運行時才確定。然而,對象可以到運行時決定從哪個裝飾類獲得額外功能。 

 

3.11    策略模式(Strategy)

Define a family of algorithms, encapsulate each one, and make them interchangeable.

定義一組算法,將每個算法都封裝起來,并且使它們之間可以互換。

 

在.NET開發中,策略模式可以方便地用委托來實現,從而大大減少類的個數。事實上,由于委托是“類型”,具體的策略函數是這個類型的對象,不同的對象可以保存在集合中,做成一個“規則庫”或者“約束庫”,比原始的基于類的策略模式更具靈活性。

  

3.12    適配器模式(Adaptor)

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

將一個類的接口變換成客戶端所期待的另一種接口,從而使原本因接口不匹配而無法在一起工作的兩個類能夠一起工作。

 

例如,關于幾何點,AutoCAD中是Point3d,DirectX中是Vector3,OpenNURBS中是On3fPoint,其他幾何庫中還有其他形式。如果想同時使用,就要用適配器模式。

  

3.13    迭代器模式(Iterator)

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

提供一種方法訪問一個容器對象中各個元素,而又不需暴露該對象的內部細節。

 

在現代編程語言如Java和C#中,迭代器模式作為語言功能提供以至于我們習以為常。據此有人建議從23種設計模式中刪除迭代器模式。

讓我們來考察C#中的迭代器模式。我們習慣于使用foreach循環,其實這得益于BCL中的IEnumerable和IEnumerator兩個接口。數組、列表、字典等數據結構為我們實現了這兩個接口(也就是實現了迭代器模式),于是我們就可以直接使用foreach循環和LINQ查詢了。當我們自己的類需要迭代時,也大可不必真的去實現這兩個接口,直接返回或組合一個集合就行了。不過在CLR引入泛型之前,確實有很多框架通過實現接口來為每一種類型創建迭代器,現在看來很臃腫。WPF就是一個例子。

CLR中的泛型與C++中的模板表面上看起來非常類似,但兩者之間其實存在一些重要的差別。

  • C++模板主要用來處理算法中數據類型的問題,例如同一套算法應用int、float、double類型的問題。而CLR泛型主要用來處理有關數據結構、函數編程以及指定基類語境下子類相互替換的問題。BCL中最常見的泛型應用就是IEnumerable, List, Tuple, Func, Action之類了。前三個是數據結構,后兩個是函數式編程,這些都是通用算法,不加泛型約束,不涉及調用泛型變量的成員。而一旦涉及調用成員,必須限制基類類型,因為C#會執行類型檢查。
  • C++模板不執行類型檢查,CLR泛型會嚴格限制類型。你無法用C#寫出C++模板常見的用法:寫一個加法類,可以分別對int、float、double做加法。因為,“T類型”沒有定義加法運算符,而你又無法找到一個定義了加法運算符的int、float、double的共同基類可以作為約束。但是VB.NET可以做到(并非不執行類型檢查,而是編譯器篡改了你的代碼,調用一個運行時助手類,可以用反射技術調用加法運算符),C#4.0中用dynamic也可以做到(不執行類型檢查,由DLR動態執行)。
  • CLR4.0支持泛型的協變和逆變,C++模板沒有類似功能。
  • CLR函數需要以Type類對象作為參數時,可以用泛型參數寫成泛型函數。

 

3.14    組合模式(Composite)

Compose objects into tree structure to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

將對象組合成樹狀結構以表示“部分-整體”的層次結構,使得用戶對單個對象和組合對象的使用具有一致性。

 

在BCL的LINQ to XML類庫(System.Xml.Linq命名空間)中,XML文檔結構是這樣表示的:

 

在WPF API中,隨處可見如下結構:

 

  

3.15    觀察者模式(Observer)

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

定義對象間的一種一對多的依賴關系,使得一個對象改變狀態,則所有依賴它的對象都會得到通知并被自動更新。

 

廣泛應用于數據綁定。 

 

3.16    門面模式(Façade)

Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use.

要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行。門面模式提供一個高層次的接口,使得子系統更易于使用。

 

門面模式在API設計中應用廣泛。實踐證明,最易于使用的API形式是“基本數據結構+分類組織的靜態方法”的模式。客戶代碼總是希望以一種簡潔的形式,最好是一個函數調用就能完成功能。顯然,“一個函數”的背后,對應API內部的許多工作,包括創建N個類的對象、復雜的交互等等,但是客戶代碼僅僅通過一個門面就可以完成所有工作。

ObjectARX作為AutoCAD的API,對普通開發者是不甚友好的,為了完成簡單的任務常常需要反復操作大量對象。為此筆者開發了AutoCADCodePack,現已在CodePlex上開源,用分類組織的靜態方法包裝了常見任務(組織成Draw/Modify/QuickSelection/CustomDictionary/Algorithm/Interaction等模塊),又通過基本數據結構的流轉保留了必要的靈活性。這個CodePack大大提高了開發效率和代碼可維護性。使用CodePack平均能減少一半的代碼行數。

 

3.17    解釋器模式(Interpretor)

Given a language, define a representation for its grammar along with a interpreter that uses the representation to interpret sentences in the language.

給定一門語言,定義它的文法的一種表示,并定義一個解釋器,使用該表示來解釋語言中的句子。

 

  • 某制圖軟件中,用斷面字符串表示道路橫斷面的配置,字符串用正則表達式解析。
  • 游戲引擎中的材質腳本讓的使用者倍感方便。
  • Windows API中關于多媒體的Winmm.dll中有一個超牛的函數,接收一個字符串命令,囊括了多媒體的方方面面。
  • Qt、WPF以及Android分別以C++、.NET和Java為開發語言,而它們使用XML定義界面。

顯然,解釋器模式對你代碼的“下家”是有利的,對你則是噩夢。如果你需要處理加減乘除表達式,你可以選擇IronPython/IronRuby等開源腳本引擎。

 

3.18    享元模式(Flyweight)

Use sharing to support large numbers of fine-grained objects efficiently.

使用共享對象可有效支持大量的細粒度對象。

 

習慣了面向對象后,你可能會覺得任何東西都可以理所當然地用對象表示,全然沒有意識到潛在的問題。事實上,當你的對象需要成千上萬時,很可能出現內存溢出。

在三維程序中,一個場景中可能存在大量三維對象,但不是所有對象都是幾何不同的。事實上,人們想盡辦法用單一模型去表示盡可能多的物體。例如游戲中大量的NPC可能使用同一模型,只是他們可能穿著不同顏色的衣服,身高略有擾動,活動在場景的不同區域,等等。

享元模式將對象的狀態分為內部狀態和外部狀態。內部狀態是可以共享的,外部狀態可以隨環境改變。

 

3.19    橋接模式(Bridge)

Decouple an abstraction from its implementation so that the two can vary independently.

將抽象和實現解耦,使得兩者可以獨立變化。

 

這個模式把抽象和實現的繼承關系改為組合。

 

4     最佳實踐

4.1    .NET技術語境下的設計模式新情況

4.1.1     枚舉數與迭代器

在C#中,foreach循環就是一個我們習以為常的語言級別的迭代器模式實現。.NET開發規范建議,盡可能用foreach循環代替for循環,而盡可能用查詢表達式代替循環。

越來越多的觀點和開發技術支持以聲明式的語法操作集合元素。雖然在底層所有元素仍需過程式迭代,但是習慣于在集合層面考慮問題(如以對集合的map操作代替循環)顯然有助于開發者提高解決問題的能力,也有利于迎接并行計算的到來(雖然底層都是迭代,但map與循環的區別是前者迫使開發者做到每次迭代互不依賴)。典型的實例包括MATLAB中將接收標量參數的函數應用于矩陣、C#和VB.NET中的LINQ以及各種函數式語言或支持函數式風格的語言中有關集合的map語法。

 

4.1.2     委托類型

委托類型對行為型模式的影響是深遠的。采用委托可進一步實踐用組合代替繼承的思路,行為型模式中的一些依賴關系被進一步消除。

  • 模板方法:用委托動態實現方法組合。
  • 觀察者:用事件委托實現通信。
  • 中介者:用委托去除中介者和各組件的耦合關系。
  • 策略:用委托代替子類表示策略。

 

4.1.3     反射技術

反射技術對創建型模式有影響。因為創建型模式致力于解決對象創建時的類型細節耦合,使得創建對象可以運行時動態地進行,而不是編譯時決定。而反射正是一種運行時操作類型的解決方案。

 

4.2    有所為,有所不為

4.2.1     模式的適用性

使用模式是自然而然的事情,多數情況下不使用是因為不需要,問題的復雜度還未達到模式的語境和作用力的要求。我們是為設計而使用模式,而不是為使用模式而設計。

在不恰當的場合使用設計模式會造成設計過度,反而降低設計的質量。一個完整的模式包括3個部分:相關的語境(Context)、與語境相關的作用力(Force)系統、問題的解決方案(Solution)。如果認為設計模式僅僅是解決方案,就會造成模式的濫用(Abuse)。只有語境和作用力完全符合時,模式中的解決方案才是最優解。在作用力平衡被打破,解決方案可能就不再適用。

 

4.2.2     使用模式的代價

在四人組的原作中,對每一種模式都給出了詳細的優點和缺點分析。從總體來看,使用設計模式的必須付出的代價包括:

  • 對象過多
  • 更復雜的裝配關系
  • 測試難度加大
  • 程序結構復雜

 

4.2.3     模式的局限性

設計模式不是法則。它不是必須遵守的,而是代表了一定條件下的一種權衡的結果、最優解。使用模式必須付出一定代價,當然在大多數情況下這種代價是可以接受的,于是才有了模式一說。

設計模式不能提高開發速度。至少其關注的中心不是開發速度,很多情況下會降低開發速度,即使正確使用了模式。從一個開發周期、一個單元模塊來看,使用模式提高了復雜度,降低了可維護性。(然而從全生命周期、整個架構來看,設計模式擁抱了變化,更清晰、可維護性更高)

 

4.3    模式指南

4.3.1     考慮使用模式

當你的項目出現了以下問題之一時,就要考慮重構,可能有模式可用:

  • 無法進行單元測試
  • 需求的變動總是導致代碼的修改
  • 有重復代碼存在
  • 繼承層次過多
  • 隱藏的依賴過多

 

4.3.2     使用了模式要告訴別人

設計規范要求:在給類命名時要體現所使用的設計模式,在注釋中也要注明。

這樣做的好處是,對熟悉設計模式的人員一眼就能明白代碼的用意,對不熟悉的人員看到注釋只需查閱相關模式也能較快理解。畢竟,使用模式使代碼局部更復雜難懂。

例如,StringBuilder暗示此類使用了建造者模式。

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()