基類的cost()方法將計算所有調味品的價錢(當然是只包括布爾值為true的調味品),子類里的cost()方法將擴展其功能,以包含特定類型飲料的價錢。
OK! 現在我們似乎已經有了一個看上去還不錯的設計,那么Central Perk的這個記賬系統就按這個設計來實現就萬事大吉了嗎?等一下,還是讓我們先從以前學習過的“找到系統中變化的部分,將變化的部分同其它穩定的部分隔開。”這個設計原則出發,重新推敲一下這個設計。
那么對于一家咖啡店來說,都有那些變化點呢?調味品的品種和價格會變嗎?咖啡的品種和價格會變嗎?咖啡和調味品的組合方式會變嗎?YES!對于一家咖啡店來說,這些方面肯定會經常發生改變的!那么,當這些改變發生的時候,我們的記賬系統要如何應對呢? 如果調味品發生改變,那么我們只能從代碼的層次重新調整Beverage基類,這太糟糕了;如果咖啡發生改變,我們可以增加或刪除一個子類即可,這個似乎還可以忍受;那么咖啡和調味品的組合方式發生改變呢?如果顧客點了一杯純黑咖啡外加兩份砂糖和一份巧克力,或者顧客點了一杯脫咖啡因咖啡(Decaf)外加三份煉乳和一份砂糖呢?我倒!突然意識到,上面的設計根本不支持組合一份以上同種調味品的情況,因為基類里的布爾值只能記錄是否包含某種調味品,而并不能表示包含幾份,連基本的功能需求都沒有滿足,看來這些開發者可以卷鋪蓋滾蛋了!(似乎他們改行去做炸彈更合適!)
好吧!讓我們來接手這個設計!我們已經分析了前面設計的失敗之處,我們應該實現支持調味品的品種和價格任意改變而不需要修改已有代碼的設計;我們還要實現支持咖啡品種和價格任意改變而不需要修改已有代碼的設計(這點上面的設計通過繼承算是實現了);還有就是支持咖啡和調味品的品種和份數任意組合而不需要修改已有代碼的設計;還有就是代碼重用越多越好了,內聚越高越好了,耦合越低越好了;(還有最重要的,報酬越高越好啦!)
看來我們要實現的目標還真不少,那么我們到底該怎么做呢?說實話,我現在也不知道!我們需要先去拜訪一下今天的主角—裝飾者模式,看看她能給我們帶來什么驚喜吧!
這就是裝飾者模式
我們還是先看一下官方的定義:
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. (裝飾者模式可以動態地給一個對象增加其他職責。就擴展對象功能來說,裝飾者模式比生成子類更為靈活。)
這里我們要重點注意那個dynamically(動態的),什么是動態?靜態又是什么?這是我們要重點區分的地方,后面我們還會專門討論這個問題。下面先看看裝飾者模式的類圖和順序圖:
Component(被裝飾對象基類)
l 定義對象的接口,可以給這些對象動態增加職責;
ConcreteComponent(具體被裝飾對象)
l 定義具體的對象,Decorator可以給它增加額外的職責;
Decorator(裝飾者抽象類)
l 維護一個指向Component實例的引用,并且定義了與Component一致的接口;
ConcreteDecorator(具體裝飾者)
l 具體的裝飾對象,給內部持有的具體被裝飾對象增加具體的職責;
怎么樣?大家都看懂了吧!看懂了我就不解釋了!— 呵呵,開玩笑的!
我們先來說說上面提到的動態和靜態的問題,所謂動態是說可以在系統運行時(RunTime)動態給對象增加其它職責而不需要修改代碼或重新編譯;所謂靜態是說必須通過調整代碼(DesignTime)才能給對象增加職責,而且系統還需要重新編譯;從具體技術層面來說,對象的組合和繼承正好對應于前面的動態和靜態,因為通過對象組合建立的交互關系不是在代碼中(DesignTime)固定死的,而是在運行時(RunTime)動態組合的;而通過繼承建立的關系是僵硬的難以改變的,因為它是在代碼中(DesignTime)固定死了的,根本不存在運行時(RunTime)改變的可能。換個角度說:我們應該多使用對象組合來保持系統的運行時擴展性,盡量少使用繼續,因為繼承讓程序變得僵硬!這句話聽著是不是很熟悉啊?恩!這就是我們前面文章里提過多次的一個設計原則:Favor composition over inheritance.(優先使用對象組合,而非類繼承),更多的就不需要再解釋了吧?
那么回到裝飾者模式,跟前面介紹過的模式一樣,裝飾者同樣是一個很簡單的模式,特別是畫出類圖和順序圖之后,一切都很清楚明了。這里只有一個地方需要特殊強調一下:Decorator是裝飾者模式里非常特殊的一個類,它既繼承于Component【IS A關系】,又維護一個指向Component實例的引用【HAS A關系】,換個角度來說,Decorator跟Component之間,既有動態組合關系又有靜態繼承關系,WHY? 這里為什么要這么來設計?上面我們說過,組合的好處是可以在運行時給對象增加職責,Decorator【HAS A】Component的目的是讓ConcreteDecorator可以在運行時動態給ConcreteComponent增加職責,這一點相對來說還比較好理解;那么Decorator繼承于Component的目的是什么?在這里,繼承的目的只有一個,那就是可以統一裝飾者和被裝飾者的接口,換個角度來說,不管是ConcretComponent還是ConcreteDecorator,它們都是 Component,用戶代碼可以把它們統一看作Component來處理,這樣帶來的更深一層的好處就是,裝飾者對象對被裝飾者對象的功能職責擴展對用戶代碼來說是完全透明的,因為用戶代碼引用的都是Component,所以就不會因為被裝飾者對象在被裝飾后,引用它的用戶代碼發生錯誤,實際上不會有任何影響,因為裝飾前后,用戶代碼引用的都是Component類型的對象,這真是太完美了!裝飾者模式通過繼承實現統一了裝飾者和被裝飾者的接口,通過組合獲得了在運行時動態擴展被裝飾者對象的能力。
我們再舉個生活中的例子,俗話說“人在衣著馬在鞍”,把這就話用裝飾者模式的語境翻譯一下,“人通過漂亮的衣服裝飾后,男人變帥了,女人變漂亮了;”。對應上面的類圖,這里人對應于ConcreteComponent,而漂亮衣服則對應于ConcreteDecorator;換個角度來說,人和漂亮衣服組合在一起【HAS A】,有了帥哥或美女,但是他們還是人【IS A】,還要做人該做的事情,但是可能會對異性更有吸引力了(擴展功能)!
現在我們已經認識了裝飾者模式,知道了動態關系和靜態關系是怎么回事,是時候該解決咖啡店的問題了,從裝飾者模式的角度來考慮問題,咖啡和調味品的關系應該是:咖啡是被裝飾對象而調味品是裝飾者,咖啡和調味品可以任意組合,但是不管怎么組合,咖啡還是咖啡!原來這么簡單啊!具體看下面的類圖:
如圖所示,Beverage還是所有飲料的基類,它對應于裝飾者模式類圖里的Component,是所有被裝飾對象的基類;HouseBlend, DarkRoast, Espresso, Decaf是具體的飲料(咖啡)種類,對應于前面的ConcreteComponent,即是具體的被裝飾對象;CondimentDecorator對應于前面的Decorator,是裝飾者的抽象類;而Milk,Mocha,Soy,Whip則都是具體的調味品,對于前面的ConcreteDecorator,也就是具體的裝飾者。下面我們通過具體的代碼再進一步理解一下基于裝飾者模式的記賬系統的實現。