免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
AOP@Work: 用 AspectJ 增強設(shè)計模式, 第 2 部分
支持復(fù)雜模式的透明組合和代碼級重用
級別: 中級
Nicholas Lesiecki (ndlesiecki@yahoo.com), 軟件工程師/編程教師, Google
2005 年 7 月 17 日
Nicholas Lesiecki 用這篇深入研究觀察者(Observer)模式的文章,繼續(xù)他對使用面向方面技術(shù)實現(xiàn)設(shè)計模式的好處的討論。他演示了 AspectJ 如何使復(fù)雜的模式轉(zhuǎn)換成可重用的基本方面,從而使框架作者能夠支持預(yù)先構(gòu)建的模式庫,供開發(fā)人員使用這些模式。
這篇文章的第 1 部分 中,我從面向方面的角度研究了兩個廣泛應(yīng)用的面向?qū)ο笤O(shè)計模式。在演示了適配器和修飾器模式在 Java 系統(tǒng)和 AspectJ 系統(tǒng)中的實現(xiàn)方式之后,我從代碼理解、重用、維護性和易于組合幾方面考慮了每種實現(xiàn)的效果。在兩種情況下,我發(fā)現(xiàn)橫切 Java 實現(xiàn)模塊性的模式在 AspectJ 實現(xiàn)中可以組合到單獨的一個方面中。理論上,這種相關(guān)代碼的協(xié)同定位可以使模式變得更易理解、更改和應(yīng)用。用這種方式看模式,就轉(zhuǎn)變對 AOP 的一個常見批評 —— 阻止開發(fā)人員通過閱讀代碼了解代碼的行為。在這篇文章的第 2 部分中,我將通過深入研究觀察者(Observer)模式,完成我對 Java 語言的模式實現(xiàn)和 AspectJ 模式實現(xiàn)的比較。
我選擇把重點放在觀察者(Observer)模式上,因為它是 OO 設(shè)計模式的皇后。該模式被人們廣泛應(yīng)用(特別是在 GUI 應(yīng)用程序中),并構(gòu)成了 MVC 架構(gòu)的關(guān)鍵部分。它處理復(fù)雜的問題,而在解決這類問題方面表現(xiàn)得相對較好。但是,從實現(xiàn)需要的努力和代碼理解的角度來說,它還是帶來了一些難以解決的難題。與修飾器或適配器模式不同(它們的參與者主要是為模式新創(chuàng)建的類),觀察者(Observer)模式要求您先侵入系統(tǒng)中現(xiàn)有的類,然后才能支持該模式 —— 至少在 Java 語言中是這樣。
方面可以降低像觀察者(Observer)模式這種侵入性模式的負(fù)擔(dān),使得模式參與者更靈活,因為不需要包含模式代碼。而且,模式本身可以變成抽象的基本方面,允許開發(fā)人員通過導(dǎo)入和應(yīng)用它來實現(xiàn)重用,不必每次都要重新考慮模式。為了查看這些可能性如何發(fā)揮作用,我將繼續(xù)本文第一部分設(shè)置的格式。我將從示例問題開始,提供對觀察者(Observer)模式的通用描述。然后我將描述如何用 AspectJ 和 Java 語言實現(xiàn)觀察者(Observer)模式。在每個實現(xiàn)之后,我將討論是什么造成模式的橫切,模式的這個版本在理解、維護、重用和組合代碼方面有什么效果。
在繼續(xù)后面的討論之前,請單擊本頁頂部或底部的 代碼 圖標(biāo),下載本文的源代碼。
根據(jù) Portland Pattern Repository Wiki(請參閱參考資料 一節(jié),獲得有關(guān)的細(xì)節(jié)),觀察者(Observer)模式的用途是
定義對象之間的一對多依賴關(guān)系,因此,當(dāng)一個對象的狀態(tài)發(fā)生改變時,其所有依賴項都會得到通知,并自動更新。
這使得觀察者適用于所有類型的通知需要。請考慮以下情況:
AOP@Work 系列是為具有一定面向方面的編程背景、并準(zhǔn)備擴展或者加深其知識的開發(fā)人員準(zhǔn)備的。與大多數(shù) developerWorks 文章一樣,本系列具有很高實用性:從每一篇文章中學(xué)到的知識立刻就能使用得上。
所挑選的為這個系列撰稿的每一位作者,都在面向方面編程方面處于領(lǐng)導(dǎo)地位或者擁有這方面的專業(yè)知識。許多作者都是本系列中討論的項目或者工具的開發(fā)人員。每篇文章都經(jīng)過仔細(xì)審查,以確保所表達的觀點的公平和準(zhǔn)確。
關(guān)于文章的意見和問題,請直接與文章的作者聯(lián)系。如果對整個系列有意見,可以與本系列的組織者Nicholas Lesiecki 聯(lián)系。更多關(guān)于 AOP 的背景知識,請參閱參考資料。
條形圖可以觀察它顯示的數(shù)據(jù)對象,以便在這些對象變化時對它們進行重新繪制。
AccountManager 對象能夠觀察 Account,這樣,在帳戶狀態(tài)改變時,它們可以向銷售人員發(fā)送一封電子郵件。
支付服務(wù)能夠觀察在線音樂商店中的歌曲播放事件,以便向客戶收費。
我將把重點放在最后一個場景上,將它作為這篇文章的示例。(這種針對問題域的想法是從 “在 .NET 中實現(xiàn)觀察者”中借鑒的;有關(guān)的細(xì)節(jié),請參閱參考資料 一節(jié)。)假設(shè)您要向在線音樂商店添加一些特性。商店已經(jīng)擁有逐個播放 Songs 的能力,還有把它們組合成在線 Playlists 的能力??蛻暨€可以查看指定歌曲的歌詞?,F(xiàn)在需要添加收費和統(tǒng)計功能。首先,系統(tǒng)應(yīng)當(dāng)跟蹤播放和歌詞顯示事件,以便對客戶進行適當(dāng)?shù)氖召M。第二,系統(tǒng)該當(dāng)更新播放最頻繁的歌曲列表,用來在“最流行”部分顯示。清單 1 包含系統(tǒng)中已經(jīng)存在的核心對象的代碼:
//common interface for items that //can be played public interface Playable { String getName(); void play(); } public class Song implements Playable{ private String name; public Song(String name) { this.name = name; } public String getName() { return name; } public void play() { System.out.println("Playing song " + getName()); } public void showLyrics(){ System.out.println("Displaying lyrics for " + getName()); } } public class Playlist implements Playable { private String name; private List songs = new ArrayList(); public Playlist(String name) { this.name = name; } public void setSongs(List songs) { this.songs = songs; } public void play() { System.out.println("playing album " + getName()); for (Song song : songs) { song.play(); } } public String getName() { return name; } } public class BillingService{ public void generateChargeFor(Playable playable) { System.out.println("generating charge for : " + playable.getName()); } } public class SongPlayCounter { public void incrementPlays(Song s){ System.out.println("Incrementing plays for " + s.getName()); } }
現(xiàn)在已經(jīng)看到了核心系統(tǒng),讓我們來考慮一下 AOP 之前的觀察者實現(xiàn)。
雖然實現(xiàn)的差異很明顯,但在它們之間還是有一些相似之處。不論如何實現(xiàn)觀察者,代碼中都必須回答以下 4 個問題:
哪個對象是主體,哪個對象是觀察者?
什么時候主體應(yīng)當(dāng)向它的觀察者發(fā)送通知?
當(dāng)接收到通知時,觀察者應(yīng)該做什么?
觀察關(guān)系應(yīng)當(dāng)在什么時候開始,什么時候終止?
我將用這些問題作為框架,帶您經(jīng)歷觀察者(Observer)模式的 OO 實現(xiàn)。
首先從標(biāo)記器接口來分配角色開始。Observer 接口只定義了一個方法:update(),它對應(yīng)著 Subject 發(fā)送通知時執(zhí)行的操作。 Subject 承擔(dān)著更多的職責(zé)。它的標(biāo)記器接口定義了兩個方法,一個用來跟蹤觀察者,另一個用來通知事件的那些觀察者。
public interface Subject { public void addObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); }
一旦定義了這些角色,就可以把它們應(yīng)用到系統(tǒng)中對應(yīng)的角色上。
可以修改 BillingService,用以下少量代碼實現(xiàn)觀察者接口:
public class BillingService implements Observer { //... public void update(Subject subject) { generateChargeFor((Playable) subject); } }
一旦這項工作完成,就可以轉(zhuǎn)移到兩個 Subject。在這里,要對 Song 進行修改:
private Set observers = new HashSet(); public void addObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { observers.remove(o); } public void notifyObservers() { for (Observer o : observers) { o.update(this); } }
現(xiàn)在面臨的是一個不太讓人高興的任務(wù):必須把 Subject 的這個實現(xiàn)剪切、粘貼到 Playlist 中。將 Subject 實現(xiàn)的一部分摘錄到一個助手類中,這有助于緩解設(shè)計的弊端,但是仍然不足以完全消除它。
這篇文章側(cè)重在 Java 語言的 OO 模式實現(xiàn)上。讀者可能想知道是否有特性更豐富的 OO 語言能夠更好地解決觀察者角色帶來的這些令人頭痛的問題。給這篇文章帶來靈感的 .NET 實現(xiàn)采用委托和事件,極大地減輕了 .NET 版本的 Song 和 Playlist 類的負(fù)擔(dān)。(要比較兩個實現(xiàn),請參閱參考資料 一節(jié)。)類似的,支持多繼承的 OO 語言也能把 Song 的負(fù)擔(dān)限制到在一個 Subject 支持類中的混合上。每種技術(shù)都有很大幫助,但是都不能處理觸發(fā)通知或組合多個模式實例的問題。我將在本文的后面部分討論這些問題的后果。
現(xiàn)在已經(jīng)把類調(diào)整到它們在模式中的角色上了。但是,還需要回過頭來,在對應(yīng)的事件發(fā)生時觸發(fā)通知。Song 要求兩個附加通知,而 Playlist 則需要一個:
//...in Song public void play() { System.out.println("Playing song " + getName()); notifyObservers(); } public void showLyrics(){ System.out.println("Displaying lyrics for " + getName()); notifyObservers(); } //...in Playlist public void play() { System.out.println("playing album " + getName()); for (Song song : songs) { //... } notifyObservers(); }
需要牢記的是,雖然示例系統(tǒng)只需要一個狀態(tài)改變通知,但是實際的系統(tǒng)可能許多許多通知。例如,我曾經(jīng)開發(fā)過一個應(yīng)用程序,該應(yīng)用程序要求在 更改購物車狀態(tài)時發(fā)送觀察者風(fēng)格的通知。為了做到這一點,我在購物車和相關(guān)對象中的 10 個以上的位置應(yīng)用了通知邏輯。
隨著各個角色準(zhǔn)備好參與到模式中,剩下來要做的就是把它們連接起來。
為了讓 BillingService 開始觀察 Song 或 Playlist,需要添加膠水代碼,由它調(diào)用 song.addObserver(billingService)。這個要求與第 1 部分 中描述的適配器和修飾器的膠水代碼的要求類似。除了影響參與者之外,模式還要求對系統(tǒng)中不確定的部分進行修改,以便激活模式。清單 2 包含的代碼模擬了客戶與系統(tǒng)的交互,并整合了這個膠水代碼,膠水代碼是突出顯示的。清單 2 還顯示了示例系統(tǒng)的輸出。
public class ObserverClient { public static void main(String[] args) { BillingService basicBilling = new BillingService(); BillingService premiumBilling = new BillingService(); Song song = new Song("Kris Kringle Was A Cat Thief"); song.addObserver(basicBilling); Song song2 = new Song("Rock n Roll McDonald's"); song2.addObserver(basicBilling); //this song is billed by two services, //perhaps the label demands an premium for online access? song2.addObserver(premiumBilling); Playlist favorites = new Playlist("Wesley Willis - Greatest Hits"); favorites.addObserver(basicBilling); List songs = new ArrayList(); songs.add(song); songs.add(song2); favorites.setSongs(songs); favorites.play(); song.showLyrics(); } } //Output: playing playlist Favorites Playing song Kris Kringle Was A Cat Thief generating charge for : Kris Kringle Was A Cat Thief Playing song Rock n Roll McDonald's generating charge for : Rock n Roll McDonald's generating charge for : Rock n Roll McDonald's generating charge for : Favorites Displaying lyrics for Kris Kringle Was A Cat Thief generating charge for : Kris Kringle Was A Cat Thief
從實現(xiàn)的步驟中,您可能感覺到,觀察者(Observer)模式是一個重量級的模式。這里的分析將探索該模式對系統(tǒng)當(dāng)前和潛在的沖擊:
橫切:在音樂商店應(yīng)用程序中,記費的觀察關(guān)注點(observation concern)既包含 記帳人員(Song 和 Playlist),又包含 開票人員(BillingService)。Java 語言實現(xiàn)還附加了膠水代碼的位置。因為它影響三個不同的類,這些類的目標(biāo)不僅僅是這個觀察關(guān)系,所以記費觀察就代表了一個橫切關(guān)注點。
易于理解:先從領(lǐng)域?qū)ο蟮囊暯情_始查看系統(tǒng)。觀察者的 OO 實現(xiàn)要求對受模式影響的領(lǐng)域類進行修改。正如在實現(xiàn)類中看到的,這些修改的工作量并不小。對于 Subject 來說,需要給每個類添加多個方法,才能讓它在模式中發(fā)揮作用。如果領(lǐng)域概念(播放列表、歌曲等)的簡單抽象被觀察者(Observer)模式的機制阻塞,又會怎么樣呢?(請參閱“有沒有更好的 OO 觀察者?”,了解為什么有些非 Java 語言可能讓這項工作更容易。)
也可以從模式的視角研究系統(tǒng)。在 Java 語言實現(xiàn)中,模式冒著“消失在代碼中”的風(fēng)險。開發(fā)人員必須檢查模式的三個方面( Observer、Subject 和連接兩者的客戶代碼),才能有意識地構(gòu)建出模式的堅固模型。如果發(fā)現(xiàn)模式的一部分,聰明的開發(fā)人員會尋找其他部分,但是沒有可以把這些部分集中在一起是“BillingObserver”模塊。
重用:要重用這個模式,必須從頭開始重新實現(xiàn)它。通常,可以利用預(yù)先構(gòu)建好的支持類(例如,java.util 包含 Observer 和 Observable )填充模式的某些部分,但是大部分工作仍然需要自己來做。
維護:為了幫助您思考觀察者(Observer)模式的維護成本,要先考慮避免雙重計費的需求。如果看一眼模式第一個實現(xiàn)的輸出(請參閱清單 2),就會看到應(yīng)用程序?qū)Σシ帕斜砩舷挛闹胁シ鸥枨挠嬞M。這并不令人感到驚訝,因為這就是編寫這段代碼要做的工作。但是,市場部門想為播放列表提供一個總體報價,以鼓勵批量購買。換句話說,他們想對播放列表計費,而不是對播放列表中的歌曲計費。
很容易就可以想像得到,這會給傳統(tǒng)的觀察者實現(xiàn)帶來麻煩。需要修改每個 play() 方法,讓它們接受一個參數(shù),表明它是由另一個 play() 方法調(diào)用的。或者,可以維護一個 ThreadLocal,用它來跟蹤這方面的信息。不管屬于哪種情況,都只在調(diào)用上下文環(huán)境得到保證的情況下才調(diào)用 notifyObservers()。這些修改可能會給已經(jīng)飽受圍困的模式參與者再添加了一些負(fù)擔(dān)和復(fù)雜性。因為變化將影響多個文件,所以在重新設(shè)計的過程中就可能出現(xiàn) bug。
組合:需要考慮的另一個場景就是不同觀察者的觀察。從設(shè)置中您可能會想起,音樂服務(wù)應(yīng)當(dāng)跟蹤最流行的歌曲。但是統(tǒng)計搜集只被應(yīng)用于歌曲播放,而不涉及歌詞查看或播放列表播放。這意味著 SongCountObserver 觀察的操作集合與記費觀察者觀察的略有不同。
要實現(xiàn)這點,就不得不修改 Song,讓它維護一個獨立的 Observer 列表,只對 play() 通知感興趣(對歌詞操作沒有興趣)。然后,play() 方法會獨立于記費事件觸發(fā)這個事件。這樣 OO 模式就保證了具體的 Subject 對具體的 Observer 的直接依賴。但是這種情況看起來是不可能的,因為 Song 必須為每種類型的 Observer 觸發(fā)不同的事件。面對著另一種 Observer 類型,Java 語言的實現(xiàn)就會崩潰。
正如您可能想到的,AspectJ 觀察者為這個分析所提出的場景提供了圓滿的解決方案。在研究它們之前,我將介紹 AspectJ 如何處理基本模式。
就像使用我在這篇文章中考慮的其他模式一樣,觀察者(Observer)模式的目的,甚至是觀察者(Observer)模式的基本結(jié)構(gòu),在用 AspectJ 實現(xiàn)時保持不變。但是,有一個關(guān)鍵的區(qū)別。通過使用面向方面的繼承,在對模式進行定制,從而滿足個人需求時,可以重用模式中對于所有實現(xiàn)都公用的那些部分。請記住,觀察者(Observer)模式實現(xiàn)必須回答以下 4 個問題:
哪個對象是主體,哪個對象是觀察者?
什么時候主體應(yīng)當(dāng)向它的觀察者發(fā)送通知?
當(dāng)接收到通知時,觀察者應(yīng)該做什么?
觀察關(guān)系應(yīng)當(dāng)在什么時候開始,什么時候終止?
首先我要回顧抽象方面的工作方式。與抽象類非常相似,在使用抽象方面之前,必須擴展它。抽象方面可以定義抽象的切入點(切入點有名稱,但是沒有主體),然后定義這些切入點上的具體的通知(advice)。這意味著由父方面指定行為,子方面控制在哪里應(yīng)用行為。另外,抽象方面還能定義子方面必須覆蓋的抽象方法,就像抽象類能夠做到的那樣。記住這些事實,現(xiàn)在來看一下來自設(shè)計模式項目 的 ObserverProtocol 方面。
就像使用其他實現(xiàn)一樣,角色由標(biāo)記器接口指定。但是,在 AspectJ 實現(xiàn)的情況下,接口是空的(可能是被 AspectJ 5 中的注釋代替)。接口以 ObserverProtocol 成員的形式出現(xiàn):
/** * This interface is used by extending aspects to say what types * can be Subjects. */ public interface Subject{} /** * This interface is used by extending aspects to say what types * can be Observers. */ public interface Observer{}
我對設(shè)計模式和 AspectJ 的狂熱開始于不列顛哥倫比亞大學(xué)贊助的面向方面設(shè)計模式實現(xiàn)項目(請參閱參考資料)。正如我在這篇文章的第一部分 提到的,Jan Hanneman 和 Gregor Kiczales 調(diào)查了 23 個 GoF 模式,并用 Java 語言和 AspectJ 為每一種模式創(chuàng)建了參考實現(xiàn)。結(jié)果非常迷人,值得研究,在原始的模式中,有 17 個模式在使用 AOP 實現(xiàn)時得到了改進。最令人興奮的是模式庫。項目的作者能夠從 13 個模式中提取出可重用的抽象方面,并在 Mozilla 公共許可下免費提供它們。本文介紹的 ObserverProtocol 方面就取自該項目。請參閱參考資料 一節(jié),以下載這些模式。
方面沒有強迫參與者跟蹤觀察者,而是把這個職責(zé)集中起來。清單 3 包含的代碼實現(xiàn)了模式的這一部分。從第 1 部分 的修飾器 AOP 實現(xiàn)中,您應(yīng)當(dāng)了解了這個通用術(shù)語。同樣,方面使用消極初始化的 Map 對特定于對象的狀態(tài)進行跟蹤。(同樣,這個模式也有包含初始化模型的替代品,但是它們超出了本文的范疇。)請注意清單 3 中的一個有趣的元素 —— addObserver 和 removeObserver 方法是公共的。這意味著系統(tǒng)中任何地方的代碼都可以用編程的方式?jīng)Q定是否參與到這個模式中。
/** * Stores the mapping between Subjects and * Observers. For each Subject, a LinkedList * is of its Observers is stored. */ private WeakHashMap perSubjectObservers; /** * Returns a Collection of the Observers of * a particular subject. Used internally. */ protected List getObservers(Subject subject) { if (perSubjectObservers == null) { perSubjectObservers = new WeakHashMap(); } List observers = (List)perSubjectObservers.get(subject); if ( observers == null ) { observers = new LinkedList(); perSubjectObservers.put(subject, observers); } return observers; } /** * Adds an Observer to a Subject. */ public void addObserver(Subject subject, Observer observer) { getObservers(subject).add(observer); } /** * Removes an observer from a Subject. */ public void removeObserver(Subject subject, Observer observer) { getObservers(subject).remove(observer); } //aspect continues...
要實現(xiàn)對觀察者的實際更新,ObserverProtocol 需要使用一個循環(huán),這非常類似于它在 Java 語言實現(xiàn)中的使用。但是,它激活通知的方式非常不同。首先,方面定義了一個抽象切入點:
protected abstract pointcut subjectChange(Subject s);
這個切入點由子方面負(fù)責(zé)具體化。ObserverProtocol 接著定義了一條通知,該通知對應(yīng)著 subjectChange() 選中的連接點:
after(Subject subject) returning : subjectChange(subject) { for (Observer observer : getObservers(subject)) { updateObserver(subject, observer); } } protected abstract void updateObserver(Subject subject, Observer observer);
對于發(fā)生變化的主角的每個 Observer,方面都需要調(diào)用 updateObserver(),這是它定義的一個抽象方法。通過這種方式,具體的方面可以定義它接收到更新時的意義。
目前,您已經(jīng)看到 ObserverProtocol 提供的所有內(nèi)容。要應(yīng)用這個模式,則需要擴展方面,并對這篇討論開始提出的 4 個核心的觀察者(Observer)模式問題提供具體的答案。下載了 ObserverProtocol 之后,就可以開始創(chuàng)建自己的方面,并聲明它擴展了 ObserverProtocol。正如您可能想到的,AspectJ 幫助器會提醒您沒有完成方面的定義。必須具體化 subjectChange 切入點,并實現(xiàn) updateObserver 方法。這些提醒可以充當(dāng)將模式應(yīng)用到系統(tǒng)中的指導(dǎo)。
使用 AspectJ,不用直接修改 Song、Playlist 和 BillingService 類,就可以把角色分配給模式的參與者,如下所示。
declare parents : Playable extends Subject; declare parents : BillingService implements Observer;
編譯器不會強迫您插入這些聲明,但是您會發(fā)現(xiàn)如果沒有它們,就無法使用其余的方面機制,因為 ObserverProtocol 將根據(jù)這些標(biāo)記器接口定義它的切入點和方法。
這可能是這個方面最重要的部分:具體化 subjectChange 切入點,定義那些操作(構(gòu)成值得通知的事件)。在這種情況下,切入點指定 play() 方法可以在任何實現(xiàn) Playable 接口的類上執(zhí)行(包括 Song 和 Playlist都可以這么做)。切入點也選擇執(zhí)行 Song 上的 showLyrics() 方法:
pointcut titleUse(Playable playable) : this(playable) && ( execution(public void Playable+.play()) || execution(public void Song.showLyrics()) ); public pointcut subjectChange(Subject subject) : titleUse(Playable) && this(subject);
非常類似于 Java 語言版本,為了響應(yīng)更新,需要實現(xiàn)更新方法。區(qū)別在于,在這里是向方面添加方法,而不是直接向 BillingService 添加,后者不知道自己參與到了模式中:
public void updateObserver(Subject s, Observer o){ BillingService service = (BillingService)o; service.generateChargeFor((Playable)s); }
正如我在前面所提到的,由于方面上存在公共的 addObserver 和 removeObserver,所以可以用編程方式對方面進行配置。要啟動對歌曲和播放列表的觀察,需要復(fù)制 Java 語言的客戶實現(xiàn),并用 ObserverBillingPolicy.aspectOf().addObserver(song, basicBilling) 替代出現(xiàn) song.addObserver(basicBilling) 的地方。
但是,為了讓模式的存在盡可能保持在不顯眼的狀態(tài),所以,使用通知自動地開始這種關(guān)系是有意義的,如下所示:
//in the concrete aspect //could be injected by a dependency injection framework like Spring //see theResources section for an excellent blog entry on this topic private BillingService defaultBillingService = new BillingService(); pointcut playableCreation(Subject s) : execution(public Playable+.new(..)) && this(s); after(Subject s) returning : playableCreation(s){ addObserver(s, defaultBillingService); }
通過使用方面中配置的默認(rèn) BillingService,該切入點和通知在創(chuàng)建 Playable 時立即開始計費觀察。但是,ObserverProtocol 使得這方面具有靈活性。請進一步想像一下:通知會將輔助計費應(yīng)用到特定歌曲上,或者檢索與系統(tǒng)當(dāng)前用法有關(guān)的計費計劃。類似的,可以用通知在歌曲不符合未來計費要求時自動終止關(guān)系。
我要做的某些設(shè)計觀察聽起來應(yīng)當(dāng)很熟悉,因為它們反映了對本文中討論的前兩個模式所做的觀察(請參閱參考資料)。但是,您會注意到,觀察者中的設(shè)計對比特別突出:
易于理解:從參與者的視角來看,AOP 版本的觀察者更簡單,因為它不需要 Song、Playlist 或 BillingService 有什么特殊的東西。從參與者那里消除了這種依賴性,從主要抽象角度來說,讓它們更容易理解,同時也使它們更加靈活、更具重用性。例如,它們可以重新用在其他不要求觀察者(Observer)模式的系統(tǒng)中。它們也可以參與到其他模式中,而不用擔(dān)心大量的模式代碼會淹沒類的核心職責(zé)。
從模式的視角來看,AOP 版本還是更簡單一些。AspectJ 實現(xiàn)不要求人們把模式片斷匯集在頭腦中,而是提供了一個 ObserverProtocol 方面,安排好了模式的結(jié)構(gòu)。然后由具體的方面定義模式應(yīng)用到這個系統(tǒng)的方式。模塊都是很實際的,而且從模式視角來看,模塊也很完整。所以,如果正在使用 AJDT,并且注意到(比如)某個觀察者方面通知說使用 play() 方法,那么就可以沿著 AJDT 提供的線索理解總體情況。(參考資料 一節(jié)包含一篇優(yōu)秀文章,其中介紹了 AJDT 如何有助于導(dǎo)航和理解 AspectJ 代碼。)
通知 “事件”的生成在 AspectJ 實現(xiàn)中獲得很好的模塊化。在傳統(tǒng)實現(xiàn)中,這些調(diào)用存在于三個不同的位置。不存在能說明為什么某個操作會觸發(fā)事件,或者其他哪個操作也會這么做的直接線索。在 AOP 模式中,命名切入點溝通了操作的公共屬性(標(biāo)題的用法)。切入點規(guī)范指出了模式影響的其他連接點。
重用:ObserverProtocol 方面的重用潛力非常高。實際上,可重用的模式方面是我編寫這篇文章的一半原因。作為開發(fā)人員,只要能依賴框架作者的經(jīng)驗,而不是折騰自己的實現(xiàn),我就會很高興。方面提供了重用橫切代碼的能力,這些代碼以前糾纏在實現(xiàn)的細(xì)節(jié)中,根本不是獨立存在的。在我分析的組合模式一節(jié)中,您會看到重用的一個很好的示例。
維護:要查看方面讓系統(tǒng)更容易發(fā)展的具體示例,請回到在系統(tǒng)的 Java 版本中將歌曲從雙重計費中排除時遇到的困難上。要用 AspectJ 滿足這個需求,只需要編輯 subjectChange() 切入點即可。以下代碼可以確保 cflowbelow() 切入點會排除另一個 play() 的控制流程中發(fā)生的 play() 執(zhí)行:
//the original titleUsePointcut pointcut titleUse(Playable playable) : this(playable) && ( execution(public void Playable+.play()) || execution(public void Song.showLyrics()) ); //exclude title usage that occurs as a result //of another title use pointcut topLevelTitleUse(Playable playable) : titleUse(playable) && ! cflowbelow(titleUse(Playable)); //define subjectChange in terms of the more restrictive //pointcut public pointcut subjectChange(Subject subject) : topLevelTitleUse(Playable) && this(subject);
這個修改是微不足道的,特別是因為它只影響一個文件。而且,它還讓新策略的意圖變得很清楚。其他修改,例如添加新 Subjects 或主角變化操作,都只需像這樣對方面進行簡單的修改即可,不需要協(xié)調(diào)多個文件的修改。
組合:可以回想一下介紹過的內(nèi)容,在與應(yīng)用到(某些)相同的參與者的第二個模式協(xié)作時,Java 語言實現(xiàn)的觀察者也存在問題。要用 AspectJ 實現(xiàn)這個需求,只要再次用新的切入點和抽象方法定義擴展抽象方面即可。由于每個方面都管理自己的 Observer 列表和 Subject 列表,而且方面也定義了自己的 subjectChange 切入點,所以兩個方面不會沖突。每個方面操作時實際上都獨立于另一個方面??梢詸z查清單 4 中 SongCountObserver 的代碼:
public aspect SongCountObserver extends ObserverProtocol { declare parents : Song extends Subject; declare parents : SongPlayCounter implements Observer; pointcut titleUse(Song song) : this(song) && execution(public void Song.play()); public pointcut subjectChange(Subject subject) : titleUse(Song) && this(subject); public void updateObserver(Subject s, Observer o) { SongPlayCounter counter = (SongPlayCounter) o; counter.incrementPlays((Song) s); } // could be injected by a dependency injection // framework like Spring private SongPlayCounter defaultCounter = new SongPlayCounter(); pointcut songCreation(Subject s) : execution(public Song.new(..)) && this(s); after(Subject s) returning : songCreation(s){ // different songs could be tracked in different statistical sets... addObserver(s, defaultCounter); } }
因為在 AspectJ 系統(tǒng)中,多個模式(甚至同一模式的多個實例)可以透明地組合在一起,所以 AspectJ 系統(tǒng)避免了來自“模式密度”的一些問題。這就能夠更多地使用設(shè)計模式所擁有的最佳實踐,而不用擔(dān)心實現(xiàn)的重量會壓跨代碼。(請繼續(xù)等待 Wes Isberg 在 AOP@Work 系列中即將推出的文章,獲取更多使用面向方面技術(shù)來避免 OO 設(shè)計模式中的模式密度問題的內(nèi)容。)
從核心上看,設(shè)計模式實際是設(shè)計問題。因為程序員都很聰明,所以這些問題已經(jīng)被解決了許多次。但也因為程序員的懶惰(用一種好的方式懶惰!),他們不愿意一次又一次地重復(fù)解決這些設(shè)計問題的工作。GoF 撰寫的書籍(以及以前的模式工作)的貢獻就是把為那些用今天的語言還不能更好表達的問題提供了通用的解決方案。這些麻煩的(如果是無價的)解決方案幾乎可以解釋為一種挑戰(zhàn),一種尋找更好(更易理解、更易重用、更易維護和更易組合)解決底層問題的方法的挑戰(zhàn)。
有些 OO 模式很難使用,因為它們解決的問題是橫切的。對于這些問題,AOP 提供的解決方案正中問題核心。通過將橫切行為搜集到單獨的一個模塊中,使行為變得更容易理解、修改和重用。因為這種類型的面向方面模式中的參與者不依賴模式代碼,所以它們也變得更靈活、更易重用。在本文中,我指出了實際的示例場景和修改,從中可以看出,在用面向方面的方式解決模式問題時,要容易得多。
新的解決方案采用了不同的形式。AOP 能在根本上把某些模式(例如修飾器)簡化到很少觀察到模式狀態(tài)的程度。其他模式(例如觀察者(Observer)模式)以可重用庫模塊的形式獲得了新生命??傆幸惶炷鷷l(fā)現(xiàn),在擴展和實現(xiàn)模式方面,會自然得就像使用今天集合框架中的類一樣。
值得進一步研究的一個迷人領(lǐng)域就是解決面向方面語言中重復(fù)出現(xiàn)問題的設(shè)計模式。從本文中您可以看到一點提示:兩個方面采用消極初始化的 map,把狀態(tài)松散地耦合到對象。除此之外,特定于 AOP 的模式(或候選模式)開始出現(xiàn)在用戶社區(qū)中。Ramnivas Laddad 在他撰寫的書籍 AspectJ 實戰(zhàn)中(請參閱參考資料)描述了幾個這樣的模式 —— 例如“工人對象”(Worker Object)和“ 蟲子洞”(Wormhole)。Stefan Hanenberg 和 Arno Schmidmeier 在他們的論文“Idioms for Building Software Frameworks in AspectJ” 已經(jīng)提出了幾個有趣的候選模式,例如“模板通知”和“切入點方法”。只有時間會告訴我們這些正在出現(xiàn)的模式是否有用,是否是一些簡單的術(shù)語,或者只是“處理 AspectJ 中破損事物的修理廠”。例如,在今年的 AOSD 會議上,我參與的一次會話討論了對“消極初始化 map”這一概念提供更好的語言支持的可能性。對今天模式的研究,可能就會促進明天的語言特性的生成。
非常感謝 Wes Isberg、Mik Kersten、Ron Bodkin 和 Ramnivas Laddad,他們審閱了早期的草稿,并提供了非常有幫助的意見和更正。
支持復(fù)雜模式的透明組合和代碼級重用
文檔選項
打印本頁
將此頁作為電子郵件發(fā)送
樣例代碼
級別: 中級
Nicholas Lesiecki (ndlesiecki@yahoo.com), 軟件工程師/編程教師, Google
2005 年 7 月 17 日
Nicholas Lesiecki 用這篇深入研究觀察者(Observer)模式的文章,繼續(xù)他對使用面向方面技術(shù)實現(xiàn)設(shè)計模式的好處的討論。他演示了 AspectJ 如何使復(fù)雜的模式轉(zhuǎn)換成可重用的基本方面,從而使框架作者能夠支持預(yù)先構(gòu)建的模式庫,供開發(fā)人員使用這些模式。
這篇文章的第 1 部分 中,我從面向方面的角度研究了兩個廣泛應(yīng)用的面向?qū)ο笤O(shè)計模式。在演示了適配器和修飾器模式在 Java 系統(tǒng)和 AspectJ 系統(tǒng)中的實現(xiàn)方式之后,我從代碼理解、重用、維護性和易于組合幾方面考慮了每種實現(xiàn)的效果。在兩種情況下,我發(fā)現(xiàn)橫切 Java 實現(xiàn)模塊性的模式在 AspectJ 實現(xiàn)中可以組合到單獨的一個方面中。理論上,這種相關(guān)代碼的協(xié)同定位可以使模式變得更易理解、更改和應(yīng)用。用這種方式看模式,就轉(zhuǎn)變對 AOP 的一個常見批評 —— 阻止開發(fā)人員通過閱讀代碼了解代碼的行為。在這篇文章的第 2 部分中,我將通過深入研究觀察者(Observer)模式,完成我對 Java 語言的模式實現(xiàn)和 AspectJ 模式實現(xiàn)的比較。
我選擇把重點放在觀察者(Observer)模式上,因為它是 OO 設(shè)計模式的皇后。該模式被人們廣泛應(yīng)用(特別是在 GUI 應(yīng)用程序中),并構(gòu)成了 MVC 架構(gòu)的關(guān)鍵部分。它處理復(fù)雜的問題,而在解決這類問題方面表現(xiàn)得相對較好。但是,從實現(xiàn)需要的努力和代碼理解的角度來說,它還是帶來了一些難以解決的難題。與修飾器或適配器模式不同(它們的參與者主要是為模式新創(chuàng)建的類),觀察者(Observer)模式要求您先侵入系統(tǒng)中現(xiàn)有的類,然后才能支持該模式 —— 至少在 Java 語言中是這樣。
方面可以降低像觀察者(Observer)模式這種侵入性模式的負(fù)擔(dān),使得模式參與者更靈活,因為不需要包含模式代碼。而且,模式本身可以變成抽象的基本方面,允許開發(fā)人員通過導(dǎo)入和應(yīng)用它來實現(xiàn)重用,不必每次都要重新考慮模式。為了查看這些可能性如何發(fā)揮作用,我將繼續(xù)本文第一部分設(shè)置的格式。我將從示例問題開始,提供對觀察者(Observer)模式的通用描述。然后我將描述如何用 AspectJ 和 Java 語言實現(xiàn)觀察者(Observer)模式。在每個實現(xiàn)之后,我將討論是什么造成模式的橫切,模式的這個版本在理解、維護、重用和組合代碼方面有什么效果。
在繼續(xù)后面的討論之前,請單擊本頁頂部或底部的 代碼 圖標(biāo),下載本文的源代碼。
根據(jù) Portland Pattern Repository Wiki(請參閱參考資料 一節(jié),獲得有關(guān)的細(xì)節(jié)),觀察者(Observer)模式的用途是
定義對象之間的一對多依賴關(guān)系,因此,當(dāng)一個對象的狀態(tài)發(fā)生改變時,其所有依賴項都會得到通知,并自動更新。
這使得觀察者適用于所有類型的通知需要。請考慮以下情況:
AOP@Work 系列是為具有一定面向方面的編程背景、并準(zhǔn)備擴展或者加深其知識的開發(fā)人員準(zhǔn)備的。與大多數(shù) developerWorks 文章一樣,本系列具有很高實用性:從每一篇文章中學(xué)到的知識立刻就能使用得上。
所挑選的為這個系列撰稿的每一位作者,都在面向方面編程方面處于領(lǐng)導(dǎo)地位或者擁有這方面的專業(yè)知識。許多作者都是本系列中討論的項目或者工具的開發(fā)人員。每篇文章都經(jīng)過仔細(xì)審查,以確保所表達的觀點的公平和準(zhǔn)確。
關(guān)于文章的意見和問題,請直接與文章的作者聯(lián)系。如果對整個系列有意見,可以與本系列的組織者Nicholas Lesiecki 聯(lián)系。更多關(guān)于 AOP 的背景知識,請參閱參考資料。
條形圖可以觀察它顯示的數(shù)據(jù)對象,以便在這些對象變化時對它們進行重新繪制。
AccountManager 對象能夠觀察 Account,這樣,在帳戶狀態(tài)改變時,它們可以向銷售人員發(fā)送一封電子郵件。
支付服務(wù)能夠觀察在線音樂商店中的歌曲播放事件,以便向客戶收費。
我將把重點放在最后一個場景上,將它作為這篇文章的示例。(這種針對問題域的想法是從 “在 .NET 中實現(xiàn)觀察者”中借鑒的;有關(guān)的細(xì)節(jié),請參閱參考資料 一節(jié)。)假設(shè)您要向在線音樂商店添加一些特性。商店已經(jīng)擁有逐個播放 Songs 的能力,還有把它們組合成在線 Playlists 的能力。客戶還可以查看指定歌曲的歌詞?,F(xiàn)在需要添加收費和統(tǒng)計功能。首先,系統(tǒng)應(yīng)當(dāng)跟蹤播放和歌詞顯示事件,以便對客戶進行適當(dāng)?shù)氖召M。第二,系統(tǒng)該當(dāng)更新播放最頻繁的歌曲列表,用來在“最流行”部分顯示。清單 1 包含系統(tǒng)中已經(jīng)存在的核心對象的代碼:
//common interface for items that //can be played public interface Playable { String getName(); void play(); } public class Song implements Playable{ private String name; public Song(String name) { this.name = name; } public String getName() { return name; } public void play() { System.out.println("Playing song " + getName()); } public void showLyrics(){ System.out.println("Displaying lyrics for " + getName()); } } public class Playlist implements Playable { private String name; private List songs = new ArrayList(); public Playlist(String name) { this.name = name; } public void setSongs(List songs) { this.songs = songs; } public void play() { System.out.println("playing album " + getName()); for (Song song : songs) { song.play(); } } public String getName() { return name; } } public class BillingService{ public void generateChargeFor(Playable playable) { System.out.println("generating charge for : " + playable.getName()); } } public class SongPlayCounter { public void incrementPlays(Song s){ System.out.println("Incrementing plays for " + s.getName()); } }
現(xiàn)在已經(jīng)看到了核心系統(tǒng),讓我們來考慮一下 AOP 之前的觀察者實現(xiàn)。
雖然實現(xiàn)的差異很明顯,但在它們之間還是有一些相似之處。不論如何實現(xiàn)觀察者,代碼中都必須回答以下 4 個問題:
哪個對象是主體,哪個對象是觀察者?
什么時候主體應(yīng)當(dāng)向它的觀察者發(fā)送通知?
當(dāng)接收到通知時,觀察者應(yīng)該做什么?
觀察關(guān)系應(yīng)當(dāng)在什么時候開始,什么時候終止?
我將用這些問題作為框架,帶您經(jīng)歷觀察者(Observer)模式的 OO 實現(xiàn)。
首先從標(biāo)記器接口來分配角色開始。Observer 接口只定義了一個方法:update(),它對應(yīng)著 Subject 發(fā)送通知時執(zhí)行的操作。 Subject 承擔(dān)著更多的職責(zé)。它的標(biāo)記器接口定義了兩個方法,一個用來跟蹤觀察者,另一個用來通知事件的那些觀察者。
public interface Subject { public void addObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); }
一旦定義了這些角色,就可以把它們應(yīng)用到系統(tǒng)中對應(yīng)的角色上。
可以修改 BillingService,用以下少量代碼實現(xiàn)觀察者接口:
public class BillingService implements Observer { //... public void update(Subject subject) { generateChargeFor((Playable) subject); } }
一旦這項工作完成,就可以轉(zhuǎn)移到兩個 Subject。在這里,要對 Song 進行修改:
private Set observers = new HashSet(); public void addObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { observers.remove(o); } public void notifyObservers() { for (Observer o : observers) { o.update(this); } }
現(xiàn)在面臨的是一個不太讓人高興的任務(wù):必須把 Subject 的這個實現(xiàn)剪切、粘貼到 Playlist 中。將 Subject 實現(xiàn)的一部分摘錄到一個助手類中,這有助于緩解設(shè)計的弊端,但是仍然不足以完全消除它。
這篇文章側(cè)重在 Java 語言的 OO 模式實現(xiàn)上。讀者可能想知道是否有特性更豐富的 OO 語言能夠更好地解決觀察者角色帶來的這些令人頭痛的問題。給這篇文章帶來靈感的 .NET 實現(xiàn)采用委托和事件,極大地減輕了 .NET 版本的 Song 和 Playlist 類的負(fù)擔(dān)。(要比較兩個實現(xiàn),請參閱參考資料 一節(jié)。)類似的,支持多繼承的 OO 語言也能把 Song 的負(fù)擔(dān)限制到在一個 Subject 支持類中的混合上。每種技術(shù)都有很大幫助,但是都不能處理觸發(fā)通知或組合多個模式實例的問題。我將在本文的后面部分討論這些問題的后果。
現(xiàn)在已經(jīng)把類調(diào)整到它們在模式中的角色上了。但是,還需要回過頭來,在對應(yīng)的事件發(fā)生時觸發(fā)通知。Song 要求兩個附加通知,而 Playlist 則需要一個:
//...in Song public void play() { System.out.println("Playing song " + getName()); notifyObservers(); } public void showLyrics(){ System.out.println("Displaying lyrics for " + getName()); notifyObservers(); } //...in Playlist public void play() { System.out.println("playing album " + getName()); for (Song song : songs) { //... } notifyObservers(); }
需要牢記的是,雖然示例系統(tǒng)只需要一個狀態(tài)改變通知,但是實際的系統(tǒng)可能許多許多通知。例如,我曾經(jīng)開發(fā)過一個應(yīng)用程序,該應(yīng)用程序要求在 更改購物車狀態(tài)時發(fā)送觀察者風(fēng)格的通知。為了做到這一點,我在購物車和相關(guān)對象中的 10 個以上的位置應(yīng)用了通知邏輯。
隨著各個角色準(zhǔn)備好參與到模式中,剩下來要做的就是把它們連接起來。
為了讓 BillingService 開始觀察 Song 或 Playlist,需要添加膠水代碼,由它調(diào)用 song.addObserver(billingService)。這個要求與第 1 部分 中描述的適配器和修飾器的膠水代碼的要求類似。除了影響參與者之外,模式還要求對系統(tǒng)中不確定的部分進行修改,以便激活模式。清單 2 包含的代碼模擬了客戶與系統(tǒng)的交互,并整合了這個膠水代碼,膠水代碼是突出顯示的。清單 2 還顯示了示例系統(tǒng)的輸出。
public class ObserverClient { public static void main(String[] args) { BillingService basicBilling = new BillingService(); BillingService premiumBilling = new BillingService(); Song song = new Song("Kris Kringle Was A Cat Thief"); song.addObserver(basicBilling); Song song2 = new Song("Rock n Roll McDonald's"); song2.addObserver(basicBilling); //this song is billed by two services, //perhaps the label demands an premium for online access? song2.addObserver(premiumBilling); Playlist favorites = new Playlist("Wesley Willis - Greatest Hits"); favorites.addObserver(basicBilling); List songs = new ArrayList(); songs.add(song); songs.add(song2); favorites.setSongs(songs); favorites.play(); song.showLyrics(); } } //Output: playing playlist Favorites Playing song Kris Kringle Was A Cat Thief generating charge for : Kris Kringle Was A Cat Thief Playing song Rock n Roll McDonald's generating charge for : Rock n Roll McDonald's generating charge for : Rock n Roll McDonald's generating charge for : Favorites Displaying lyrics for Kris Kringle Was A Cat Thief generating charge for : Kris Kringle Was A Cat Thief
從實現(xiàn)的步驟中,您可能感覺到,觀察者(Observer)模式是一個重量級的模式。這里的分析將探索該模式對系統(tǒng)當(dāng)前和潛在的沖擊:
橫切:在音樂商店應(yīng)用程序中,記費的觀察關(guān)注點(observation concern)既包含 記帳人員(Song 和 Playlist),又包含 開票人員(BillingService)。Java 語言實現(xiàn)還附加了膠水代碼的位置。因為它影響三個不同的類,這些類的目標(biāo)不僅僅是這個觀察關(guān)系,所以記費觀察就代表了一個橫切關(guān)注點。
易于理解:先從領(lǐng)域?qū)ο蟮囊暯情_始查看系統(tǒng)。觀察者的 OO 實現(xiàn)要求對受模式影響的領(lǐng)域類進行修改。正如在實現(xiàn)類中看到的,這些修改的工作量并不小。對于 Subject 來說,需要給每個類添加多個方法,才能讓它在模式中發(fā)揮作用。如果領(lǐng)域概念(播放列表、歌曲等)的簡單抽象被觀察者(Observer)模式的機制阻塞,又會怎么樣呢?(請參閱“有沒有更好的 OO 觀察者?”,了解為什么有些非 Java 語言可能讓這項工作更容易。)
也可以從模式的視角研究系統(tǒng)。在 Java 語言實現(xiàn)中,模式冒著“消失在代碼中”的風(fēng)險。開發(fā)人員必須檢查模式的三個方面( Observer、Subject 和連接兩者的客戶代碼),才能有意識地構(gòu)建出模式的堅固模型。如果發(fā)現(xiàn)模式的一部分,聰明的開發(fā)人員會尋找其他部分,但是沒有可以把這些部分集中在一起是“BillingObserver”模塊。
重用:要重用這個模式,必須從頭開始重新實現(xiàn)它。通常,可以利用預(yù)先構(gòu)建好的支持類(例如,java.util 包含 Observer 和 Observable )填充模式的某些部分,但是大部分工作仍然需要自己來做。
維護:為了幫助您思考觀察者(Observer)模式的維護成本,要先考慮避免雙重計費的需求。如果看一眼模式第一個實現(xiàn)的輸出(請參閱清單 2),就會看到應(yīng)用程序?qū)Σシ帕斜砩舷挛闹胁シ鸥枨挠嬞M。這并不令人感到驚訝,因為這就是編寫這段代碼要做的工作。但是,市場部門想為播放列表提供一個總體報價,以鼓勵批量購買。換句話說,他們想對播放列表計費,而不是對播放列表中的歌曲計費。
很容易就可以想像得到,這會給傳統(tǒng)的觀察者實現(xiàn)帶來麻煩。需要修改每個 play() 方法,讓它們接受一個參數(shù),表明它是由另一個 play() 方法調(diào)用的?;蛘撸梢跃S護一個 ThreadLocal,用它來跟蹤這方面的信息。不管屬于哪種情況,都只在調(diào)用上下文環(huán)境得到保證的情況下才調(diào)用 notifyObservers()。這些修改可能會給已經(jīng)飽受圍困的模式參與者再添加了一些負(fù)擔(dān)和復(fù)雜性。因為變化將影響多個文件,所以在重新設(shè)計的過程中就可能出現(xiàn) bug。
組合:需要考慮的另一個場景就是不同觀察者的觀察。從設(shè)置中您可能會想起,音樂服務(wù)應(yīng)當(dāng)跟蹤最流行的歌曲。但是統(tǒng)計搜集只被應(yīng)用于歌曲播放,而不涉及歌詞查看或播放列表播放。這意味著 SongCountObserver 觀察的操作集合與記費觀察者觀察的略有不同。
要實現(xiàn)這點,就不得不修改 Song,讓它維護一個獨立的 Observer 列表,只對 play() 通知感興趣(對歌詞操作沒有興趣)。然后,play() 方法會獨立于記費事件觸發(fā)這個事件。這樣 OO 模式就保證了具體的 Subject 對具體的 Observer 的直接依賴。但是這種情況看起來是不可能的,因為 Song 必須為每種類型的 Observer 觸發(fā)不同的事件。面對著另一種 Observer 類型,Java 語言的實現(xiàn)就會崩潰。
正如您可能想到的,AspectJ 觀察者為這個分析所提出的場景提供了圓滿的解決方案。在研究它們之前,我將介紹 AspectJ 如何處理基本模式。
就像使用我在這篇文章中考慮的其他模式一樣,觀察者(Observer)模式的目的,甚至是觀察者(Observer)模式的基本結(jié)構(gòu),在用 AspectJ 實現(xiàn)時保持不變。但是,有一個關(guān)鍵的區(qū)別。通過使用面向方面的繼承,在對模式進行定制,從而滿足個人需求時,可以重用模式中對于所有實現(xiàn)都公用的那些部分。請記住,觀察者(Observer)模式實現(xiàn)必須回答以下 4 個問題:
哪個對象是主體,哪個對象是觀察者?
什么時候主體應(yīng)當(dāng)向它的觀察者發(fā)送通知?
當(dāng)接收到通知時,觀察者應(yīng)該做什么?
觀察關(guān)系應(yīng)當(dāng)在什么時候開始,什么時候終止?
首先我要回顧抽象方面的工作方式。與抽象類非常相似,在使用抽象方面之前,必須擴展它。抽象方面可以定義抽象的切入點(切入點有名稱,但是沒有主體),然后定義這些切入點上的具體的通知(advice)。這意味著由父方面指定行為,子方面控制在哪里應(yīng)用行為。另外,抽象方面還能定義子方面必須覆蓋的抽象方法,就像抽象類能夠做到的那樣。記住這些事實,現(xiàn)在來看一下來自設(shè)計模式項目 的 ObserverProtocol 方面。
就像使用其他實現(xiàn)一樣,角色由標(biāo)記器接口指定。但是,在 AspectJ 實現(xiàn)的情況下,接口是空的(可能是被 AspectJ 5 中的注釋代替)。接口以 ObserverProtocol 成員的形式出現(xiàn):
/** * This interface is used by extending aspects to say what types * can be Subjects. */ public interface Subject{} /** * This interface is used by extending aspects to say what types * can be Observers. */ public interface Observer{}
我對設(shè)計模式和 AspectJ 的狂熱開始于不列顛哥倫比亞大學(xué)贊助的面向方面設(shè)計模式實現(xiàn)項目(請參閱參考資料)。正如我在這篇文章的第一部分 提到的,Jan Hanneman 和 Gregor Kiczales 調(diào)查了 23 個 GoF 模式,并用 Java 語言和 AspectJ 為每一種模式創(chuàng)建了參考實現(xiàn)。結(jié)果非常迷人,值得研究,在原始的模式中,有 17 個模式在使用 AOP 實現(xiàn)時得到了改進。最令人興奮的是模式庫。項目的作者能夠從 13 個模式中提取出可重用的抽象方面,并在 Mozilla 公共許可下免費提供它們。本文介紹的 ObserverProtocol 方面就取自該項目。請參閱參考資料 一節(jié),以下載這些模式。
方面沒有強迫參與者跟蹤觀察者,而是把這個職責(zé)集中起來。清單 3 包含的代碼實現(xiàn)了模式的這一部分。從第 1 部分 的修飾器 AOP 實現(xiàn)中,您應(yīng)當(dāng)了解了這個通用術(shù)語。同樣,方面使用消極初始化的 Map 對特定于對象的狀態(tài)進行跟蹤。(同樣,這個模式也有包含初始化模型的替代品,但是它們超出了本文的范疇。)請注意清單 3 中的一個有趣的元素 —— addObserver 和 removeObserver 方法是公共的。這意味著系統(tǒng)中任何地方的代碼都可以用編程的方式?jīng)Q定是否參與到這個模式中。
/** * Stores the mapping between Subjects and * Observers. For each Subject, a LinkedList * is of its Observers is stored. */ private WeakHashMap perSubjectObservers; /** * Returns a Collection of the Observers of * a particular subject. Used internally. */ protected List getObservers(Subject subject) { if (perSubjectObservers == null) { perSubjectObservers = new WeakHashMap(); } List observers = (List)perSubjectObservers.get(subject); if ( observers == null ) { observers = new LinkedList(); perSubjectObservers.put(subject, observers); } return observers; } /** * Adds an Observer to a Subject. */ public void addObserver(Subject subject, Observer observer) { getObservers(subject).add(observer); } /** * Removes an observer from a Subject. */ public void removeObserver(Subject subject, Observer observer) { getObservers(subject).remove(observer); } //aspect continues...
要實現(xiàn)對觀察者的實際更新,ObserverProtocol 需要使用一個循環(huán),這非常類似于它在 Java 語言實現(xiàn)中的使用。但是,它激活通知的方式非常不同。首先,方面定義了一個抽象切入點:
protected abstract pointcut subjectChange(Subject s);
這個切入點由子方面負(fù)責(zé)具體化。ObserverProtocol 接著定義了一條通知,該通知對應(yīng)著 subjectChange() 選中的連接點:
after(Subject subject) returning : subjectChange(subject) { for (Observer observer : getObservers(subject)) { updateObserver(subject, observer); } } protected abstract void updateObserver(Subject subject, Observer observer);
對于發(fā)生變化的主角的每個 Observer,方面都需要調(diào)用 updateObserver(),這是它定義的一個抽象方法。通過這種方式,具體的方面可以定義它接收到更新時的意義。
目前,您已經(jīng)看到 ObserverProtocol 提供的所有內(nèi)容。要應(yīng)用這個模式,則需要擴展方面,并對這篇討論開始提出的 4 個核心的觀察者(Observer)模式問題提供具體的答案。下載了 ObserverProtocol 之后,就可以開始創(chuàng)建自己的方面,并聲明它擴展了 ObserverProtocol。正如您可能想到的,AspectJ 幫助器會提醒您沒有完成方面的定義。必須具體化 subjectChange 切入點,并實現(xiàn) updateObserver 方法。這些提醒可以充當(dāng)將模式應(yīng)用到系統(tǒng)中的指導(dǎo)。
使用 AspectJ,不用直接修改 Song、Playlist 和 BillingService 類,就可以把角色分配給模式的參與者,如下所示。
declare parents : Playable extends Subject; declare parents : BillingService implements Observer;
編譯器不會強迫您插入這些聲明,但是您會發(fā)現(xiàn)如果沒有它們,就無法使用其余的方面機制,因為 ObserverProtocol 將根據(jù)這些標(biāo)記器接口定義它的切入點和方法。
這可能是這個方面最重要的部分:具體化 subjectChange 切入點,定義那些操作(構(gòu)成值得通知的事件)。在這種情況下,切入點指定 play() 方法可以在任何實現(xiàn) Playable 接口的類上執(zhí)行(包括 Song 和 Playlist都可以這么做)。切入點也選擇執(zhí)行 Song 上的 showLyrics() 方法:
pointcut titleUse(Playable playable) : this(playable) && ( execution(public void Playable+.play()) || execution(public void Song.showLyrics()) ); public pointcut subjectChange(Subject subject) : titleUse(Playable) && this(subject);
非常類似于 Java 語言版本,為了響應(yīng)更新,需要實現(xiàn)更新方法。區(qū)別在于,在這里是向方面添加方法,而不是直接向 BillingService 添加,后者不知道自己參與到了模式中:
public void updateObserver(Subject s, Observer o){ BillingService service = (BillingService)o; service.generateChargeFor((Playable)s); }
正如我在前面所提到的,由于方面上存在公共的 addObserver 和 removeObserver,所以可以用編程方式對方面進行配置。要啟動對歌曲和播放列表的觀察,需要復(fù)制 Java 語言的客戶實現(xiàn),并用 ObserverBillingPolicy.aspectOf().addObserver(song, basicBilling) 替代出現(xiàn) song.addObserver(basicBilling) 的地方。
但是,為了讓模式的存在盡可能保持在不顯眼的狀態(tài),所以,使用通知自動地開始這種關(guān)系是有意義的,如下所示:
//in the concrete aspect //could be injected by a dependency injection framework like Spring //see theResources section for an excellent blog entry on this topic private BillingService defaultBillingService = new BillingService(); pointcut playableCreation(Subject s) : execution(public Playable+.new(..)) && this(s); after(Subject s) returning : playableCreation(s){ addObserver(s, defaultBillingService); }
通過使用方面中配置的默認(rèn) BillingService,該切入點和通知在創(chuàng)建 Playable 時立即開始計費觀察。但是,ObserverProtocol 使得這方面具有靈活性。請進一步想像一下:通知會將輔助計費應(yīng)用到特定歌曲上,或者檢索與系統(tǒng)當(dāng)前用法有關(guān)的計費計劃。類似的,可以用通知在歌曲不符合未來計費要求時自動終止關(guān)系。
我要做的某些設(shè)計觀察聽起來應(yīng)當(dāng)很熟悉,因為它們反映了對本文中討論的前兩個模式所做的觀察(請參閱參考資料)。但是,您會注意到,觀察者中的設(shè)計對比特別突出:
易于理解:從參與者的視角來看,AOP 版本的觀察者更簡單,因為它不需要 Song、Playlist 或 BillingService 有什么特殊的東西。從參與者那里消除了這種依賴性,從主要抽象角度來說,讓它們更容易理解,同時也使它們更加靈活、更具重用性。例如,它們可以重新用在其他不要求觀察者(Observer)模式的系統(tǒng)中。它們也可以參與到其他模式中,而不用擔(dān)心大量的模式代碼會淹沒類的核心職責(zé)。
從模式的視角來看,AOP 版本還是更簡單一些。AspectJ 實現(xiàn)不要求人們把模式片斷匯集在頭腦中,而是提供了一個 ObserverProtocol 方面,安排好了模式的結(jié)構(gòu)。然后由具體的方面定義模式應(yīng)用到這個系統(tǒng)的方式。模塊都是很實際的,而且從模式視角來看,模塊也很完整。所以,如果正在使用 AJDT,并且注意到(比如)某個觀察者方面通知說使用 play() 方法,那么就可以沿著 AJDT 提供的線索理解總體情況。(參考資料 一節(jié)包含一篇優(yōu)秀文章,其中介紹了 AJDT 如何有助于導(dǎo)航和理解 AspectJ 代碼。)
通知 “事件”的生成在 AspectJ 實現(xiàn)中獲得很好的模塊化。在傳統(tǒng)實現(xiàn)中,這些調(diào)用存在于三個不同的位置。不存在能說明為什么某個操作會觸發(fā)事件,或者其他哪個操作也會這么做的直接線索。在 AOP 模式中,命名切入點溝通了操作的公共屬性(標(biāo)題的用法)。切入點規(guī)范指出了模式影響的其他連接點。
重用:ObserverProtocol 方面的重用潛力非常高。實際上,可重用的模式方面是我編寫這篇文章的一半原因。作為開發(fā)人員,只要能依賴框架作者的經(jīng)驗,而不是折騰自己的實現(xiàn),我就會很高興。方面提供了重用橫切代碼的能力,這些代碼以前糾纏在實現(xiàn)的細(xì)節(jié)中,根本不是獨立存在的。在我分析的組合模式一節(jié)中,您會看到重用的一個很好的示例。
維護:要查看方面讓系統(tǒng)更容易發(fā)展的具體示例,請回到在系統(tǒng)的 Java 版本中將歌曲從雙重計費中排除時遇到的困難上。要用 AspectJ 滿足這個需求,只需要編輯 subjectChange() 切入點即可。以下代碼可以確保 cflowbelow() 切入點會排除另一個 play() 的控制流程中發(fā)生的 play() 執(zhí)行:
//the original titleUsePointcut pointcut titleUse(Playable playable) : this(playable) && ( execution(public void Playable+.play()) || execution(public void Song.showLyrics()) ); //exclude title usage that occurs as a result //of another title use pointcut topLevelTitleUse(Playable playable) : titleUse(playable) && ! cflowbelow(titleUse(Playable)); //define subjectChange in terms of the more restrictive //pointcut public pointcut subjectChange(Subject subject) : topLevelTitleUse(Playable) && this(subject);
這個修改是微不足道的,特別是因為它只影響一個文件。而且,它還讓新策略的意圖變得很清楚。其他修改,例如添加新 Subjects 或主角變化操作,都只需像這樣對方面進行簡單的修改即可,不需要協(xié)調(diào)多個文件的修改。
組合:可以回想一下介紹過的內(nèi)容,在與應(yīng)用到(某些)相同的參與者的第二個模式協(xié)作時,Java 語言實現(xiàn)的觀察者也存在問題。要用 AspectJ 實現(xiàn)這個需求,只要再次用新的切入點和抽象方法定義擴展抽象方面即可。由于每個方面都管理自己的 Observer 列表和 Subject 列表,而且方面也定義了自己的 subjectChange 切入點,所以兩個方面不會沖突。每個方面操作時實際上都獨立于另一個方面。可以檢查清單 4 中 SongCountObserver 的代碼:
public aspect SongCountObserver extends ObserverProtocol { declare parents : Song extends Subject; declare parents : SongPlayCounter implements Observer; pointcut titleUse(Song song) : this(song) && execution(public void Song.play()); public pointcut subjectChange(Subject subject) : titleUse(Song) && this(subject); public void updateObserver(Subject s, Observer o) { SongPlayCounter counter = (SongPlayCounter) o; counter.incrementPlays((Song) s); } // could be injected by a dependency injection // framework like Spring private SongPlayCounter defaultCounter = new SongPlayCounter(); pointcut songCreation(Subject s) : execution(public Song.new(..)) && this(s); after(Subject s) returning : songCreation(s){ // different songs could be tracked in different statistical sets... addObserver(s, defaultCounter); } }
因為在 AspectJ 系統(tǒng)中,多個模式(甚至同一模式的多個實例)可以透明地組合在一起,所以 AspectJ 系統(tǒng)避免了來自“模式密度”的一些問題。這就能夠更多地使用設(shè)計模式所擁有的最佳實踐,而不用擔(dān)心實現(xiàn)的重量會壓跨代碼。(請繼續(xù)等待 Wes Isberg 在 AOP@Work 系列中即將推出的文章,獲取更多使用面向方面技術(shù)來避免 OO 設(shè)計模式中的模式密度問題的內(nèi)容。)
從核心上看,設(shè)計模式實際是設(shè)計問題。因為程序員都很聰明,所以這些問題已經(jīng)被解決了許多次。但也因為程序員的懶惰(用一種好的方式懶惰!),他們不愿意一次又一次地重復(fù)解決這些設(shè)計問題的工作。GoF 撰寫的書籍(以及以前的模式工作)的貢獻就是把為那些用今天的語言還不能更好表達的問題提供了通用的解決方案。這些麻煩的(如果是無價的)解決方案幾乎可以解釋為一種挑戰(zhàn),一種尋找更好(更易理解、更易重用、更易維護和更易組合)解決底層問題的方法的挑戰(zhàn)。
有些 OO 模式很難使用,因為它們解決的問題是橫切的。對于這些問題,AOP 提供的解決方案正中問題核心。通過將橫切行為搜集到單獨的一個模塊中,使行為變得更容易理解、修改和重用。因為這種類型的面向方面模式中的參與者不依賴模式代碼,所以它們也變得更靈活、更易重用。在本文中,我指出了實際的示例場景和修改,從中可以看出,在用面向方面的方式解決模式問題時,要容易得多。
新的解決方案采用了不同的形式。AOP 能在根本上把某些模式(例如修飾器)簡化到很少觀察到模式狀態(tài)的程度。其他模式(例如觀察者(Observer)模式)以可重用庫模塊的形式獲得了新生命??傆幸惶炷鷷l(fā)現(xiàn),在擴展和實現(xiàn)模式方面,會自然得就像使用今天集合框架中的類一樣。
值得進一步研究的一個迷人領(lǐng)域就是解決面向方面語言中重復(fù)出現(xiàn)問題的設(shè)計模式。從本文中您可以看到一點提示:兩個方面采用消極初始化的 map,把狀態(tài)松散地耦合到對象。除此之外,特定于 AOP 的模式(或候選模式)開始出現(xiàn)在用戶社區(qū)中。Ramnivas Laddad 在他撰寫的書籍 AspectJ 實戰(zhàn)中(請參閱參考資料)描述了幾個這樣的模式 —— 例如“工人對象”(Worker Object)和“ 蟲子洞”(Wormhole)。Stefan Hanenberg 和 Arno Schmidmeier 在他們的論文“Idioms for Building Software Frameworks in AspectJ” 已經(jīng)提出了幾個有趣的候選模式,例如“模板通知”和“切入點方法”。只有時間會告訴我們這些正在出現(xiàn)的模式是否有用,是否是一些簡單的術(shù)語,或者只是“處理 AspectJ 中破損事物的修理廠”。例如,在今年的 AOSD 會議上,我參與的一次會話討論了對“消極初始化 map”這一概念提供更好的語言支持的可能性。對今天模式的研究,可能就會促進明天的語言特性的生成。
非常感謝 Wes Isberg、Mik Kersten、Ron Bodkin 和 Ramnivas Laddad,他們審閱了早期的草稿,并提供了非常有幫助的意見和更正。
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
觀察者(Observer)模式
設(shè)計模式之:解剖觀察者模式
觀察者模式(Observer Pattern)
簡說設(shè)計模式——觀察者模式
觀察者模式——氣象局高溫警告
PHP設(shè)計模式之觀察者模式
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服