Java-Android-jwebee
          Java-Android-jwebee
          對(duì)IT人來(lái)說(shuō),要成為一個(gè)優(yōu)秀的技術(shù)型管理者,除了需要具備扎實(shí)的技術(shù)基礎(chǔ)之外,還應(yīng)該培養(yǎng)良好的人際關(guān)系能力、談判與溝通技能、客戶關(guān)系與咨詢技能、商業(yè)頭腦和財(cái)務(wù)技能以及創(chuàng)新意識(shí),此外還要有巧妙的激勵(lì)技巧和化解沖突與解決突發(fā)問(wèn)題的能力.

          作者 Mats Helander譯者 王麗娟 發(fā)布于 2008年2月27日 下午11時(shí)9分

          社區(qū)
          Architecture
          主題
          AOP

          導(dǎo)言

          正如從像《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》[Evans DDD]和《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)和模式應(yīng)用[Nilsson ADDDP]這些書中學(xué)到的一樣,在應(yīng)用架構(gòu)中引入領(lǐng)域模型模式(《企業(yè)應(yīng)用架構(gòu)模式》[Fowler PoEAA])一定會(huì)有很多益處,但是它們并不是無(wú)代價(jià)的。

           

          使用領(lǐng)域模型,很少會(huì)像創(chuàng)建實(shí)際領(lǐng)域模型類、然后使用它們那么簡(jiǎn)單。很快你就會(huì)發(fā)現(xiàn),領(lǐng)域模型必須得到相當(dāng)數(shù)量的基礎(chǔ)架構(gòu)代碼的支持。

          領(lǐng)域模型所需基礎(chǔ)架構(gòu)當(dāng)中最顯著的當(dāng)然是持久化——通常是持久化到關(guān)系型數(shù)據(jù)庫(kù)中,也就是對(duì)象/關(guān)系(O/R)映射出場(chǎng)的地方。但是,情況并不止持久化那么簡(jiǎn)單。在一個(gè)復(fù)雜的應(yīng)用中,用來(lái)在運(yùn)行時(shí)管理領(lǐng)域模型對(duì)象的部分占了基礎(chǔ)架構(gòu)的很大一部分。我將基礎(chǔ)架構(gòu)的這部分稱為領(lǐng)域模型管理(Domain Model Management)[Helander DMM],或簡(jiǎn)稱為DMM。

          基礎(chǔ)架構(gòu)代碼放在哪里?

          隨著基礎(chǔ)架構(gòu)代碼的增長(zhǎng),找到一個(gè)處理它的優(yōu)良架構(gòu)變得越來(lái)越重要。問(wèn)題主要在于——我們是否允許把一些基礎(chǔ)架構(gòu)代碼放在我們的領(lǐng)域模型類里面,還是無(wú)論如何應(yīng)該避免這樣做?

          避免基礎(chǔ)架構(gòu)代碼進(jìn)入領(lǐng)域模型類的論點(diǎn)是強(qiáng)有力的:領(lǐng)域模型應(yīng)該表示應(yīng)用程序所處理的核心業(yè)務(wù)概念。對(duì)于想大量使用其領(lǐng)域模型的應(yīng)用來(lái)說(shuō),保持這些類干凈、輕量級(jí)、易于維護(hù)是一個(gè)極好的架構(gòu)目標(biāo)。

          另一方面,我們接下來(lái)將會(huì)看到,保持領(lǐng)域模型類完全不含基礎(chǔ)架構(gòu)代碼——通常被稱為使用POJO/POCO(Plain Old Java/CLR Objects)領(lǐng)域模型,這種極端的路線也被證明是有問(wèn)題的。最終往往導(dǎo)致采用笨重的、低效率的變通方法來(lái)解決問(wèn)題——而且有些功能用這種方式根本不可能實(shí)現(xiàn)。

          也就是說(shuō),我們遇到的還是一個(gè)權(quán)衡利弊的情況,我們應(yīng)該盡量在領(lǐng)域模型類里面只放必不可少的基礎(chǔ)架構(gòu)代碼,決不超出這個(gè)限度。我們付出領(lǐng)域模型的輕微發(fā)胖,換來(lái)效率的提高以及使一些必要領(lǐng)域模型管理功能有可能實(shí)現(xiàn)。畢竟,軟件架構(gòu)很大程度上是關(guān)于如何做一筆好買賣。

          重構(gòu)的時(shí)機(jī)到了

          不幸的是,長(zhǎng)遠(yuǎn)看來(lái),臺(tái)面上的交易條件可能不夠好。為了支持許多最有用和最強(qiáng)大的功能,你需要在領(lǐng)域模型類中放入基礎(chǔ)架構(gòu)代碼實(shí)在太多了。其數(shù)量之大,很可能你的系統(tǒng)還沒(méi)完成,業(yè)務(wù)邏輯代碼就已經(jīng)被淹沒(méi)了。

          也就是說(shuō),除非我們能找到一種方法魚和熊掌兼得。本文試圖分析我們能否找到這樣一種方式,既能將必要的基礎(chǔ)架構(gòu)代碼分布到領(lǐng)域模型中,卻又不會(huì)使領(lǐng)域模型類變得雜亂。

          我們先從一個(gè)應(yīng)用看起,它將所有有關(guān)的基礎(chǔ)架構(gòu)代碼都放到了領(lǐng)域模型類中。接著我們將重構(gòu)這個(gè)應(yīng)用,并且只用眾所周知的、可靠的、真正面向?qū)ο蟮脑O(shè)計(jì)模式,使應(yīng)用最后能具備相同的功能,但是基礎(chǔ)架構(gòu)代碼卻不會(huì)弄亂領(lǐng)域模型類。最后,我們將看看我們?nèi)绾问褂妹嫦蚍矫婢幊蹋ˋspect Oriented Programming,AOP)來(lái)更簡(jiǎn)單地達(dá)到相同的效果。

          但是,為了看出AOP為何能幫助我們處理DMM需求,我們首先看看沒(méi)有AOP的時(shí)候我們的代碼會(huì)是什么樣——首先是“最原始”的形式,這種形式里,所有的基礎(chǔ)架構(gòu)代碼都放在領(lǐng)域模型類里面,然后是重構(gòu)后的形式,其中基礎(chǔ)架構(gòu)代碼已經(jīng)被分離出領(lǐng)域模型類——雖然仍然分布在領(lǐng)域模型中!

          重構(gòu)肥領(lǐng)域模型

          大部分的領(lǐng)域模型運(yùn)行時(shí)管理是基于攔截的——也就是說(shuō),當(dāng)你在代碼中訪問(wèn)領(lǐng)域模型對(duì)象時(shí),你所有對(duì)對(duì)象的訪問(wèn)都會(huì)根據(jù)相應(yīng)功能的需要被攔截下來(lái)。

          一個(gè)明顯的例子就是臟跟蹤(dirty tracking。它可以用于應(yīng)用的很多部分,以了解一個(gè)對(duì)象什么時(shí)候已經(jīng)被修改了、但是仍未保存(它處于“臟”狀態(tài))。用戶界面可以利用該信息提醒用戶是否打算放棄任何未保存的修改,而持久化機(jī)制則可以利用它來(lái)辨明哪些對(duì)象是真正需要被保存到持久化介質(zhì)中的,從而避免保存所有的對(duì)象。

          臟跟蹤的一種方法是保持領(lǐng)域?qū)ο笞畛醯摹⑽葱薷陌姹镜目截悾⒃诿看蜗胫酪粋€(gè)對(duì)象是否已經(jīng)被修改的時(shí)候去比較它們。這個(gè)方案的問(wèn)題是既浪費(fèi)內(nèi)存,又慢。一個(gè)更有效率的方法是攔截對(duì)領(lǐng)域?qū)ο髎etter方法的調(diào)用,以便每當(dāng)調(diào)用對(duì)象的一個(gè)setter方法的時(shí)候,都為該對(duì)象設(shè)置一個(gè)臟標(biāo)記。

          臟標(biāo)記放在哪里?

          現(xiàn)在我們來(lái)看看把臟標(biāo)記放在哪里的問(wèn)題。一種是將它放在一個(gè)字典結(jié)構(gòu)中,對(duì)象和標(biāo)記分別作為鍵和值。這樣做的問(wèn)題在于,我們必須讓程序中所有需要它的部分都能訪問(wèn)到這個(gè)字典。前面的例子已經(jīng)可以看出,需要訪問(wèn)字典的包括用戶界面和持久化機(jī)制這樣截然不同的部分。


          圖 1

          將字典放在這些組件的任何一個(gè)內(nèi)部,都會(huì)使其它組件難以訪問(wèn)它。在分層結(jié)構(gòu)中,底層不能調(diào)用其上層(除了中心領(lǐng)域模型,它常常處于一個(gè)公共的、垂直的層里面,能被其它所有的層調(diào)用),因此要么把字典放在需要訪問(wèn)它的最低一層(圖1),要么放在公共的、垂直的層里面(圖2)。兩種選擇都不是很有吸引力,因?yàn)樗鹆藨?yīng)用組件間不必要的耦合和不均衡的責(zé)任分配。


          圖 2

          一個(gè)更吸引人的、順應(yīng)面向?qū)ο笏枷氲倪x擇,是將臟標(biāo)記放到領(lǐng)域?qū)ο蟊旧碇腥ィ@樣每個(gè)領(lǐng)域?qū)ο蠖紟в幸粋€(gè)布爾型的臟屬性,來(lái)表明它是不是臟的(圖3)。這樣,任何組件想知道一個(gè)領(lǐng)域?qū)ο笈K與否,可以直接問(wèn)它。


          圖 3

          因此,我們把部分基礎(chǔ)架構(gòu)功能代碼放在領(lǐng)域模型中,其部分原因就是我們希望從應(yīng)用的不同部分都能擁有這些功能,而不會(huì)過(guò)度地增強(qiáng)耦合。用戶界面部分不該知道如何向持久化組件詢問(wèn)臟標(biāo)志,并且,我們寧愿在分層的應(yīng)用架構(gòu)中設(shè)計(jì)盡可能少的垂直層。

          這個(gè)理由很重要,單憑它就足以讓一些人考慮采納本文將要檢驗(yàn)的這種方法,不過(guò)我們還是先看看其他方法。但是在這樣做之前,我們先粗略地看一下?tīng)?zhēng)論的另一方——我們?cè)陬I(lǐng)域模型類中限制基礎(chǔ)架構(gòu)代碼的原因。

          肥領(lǐng)域模型反模式

          讓我們看看,引入臟標(biāo)記、并在適當(dāng)時(shí)機(jī)要求攔截喚醒臟標(biāo)記之后,領(lǐng)域類會(huì)是什么樣子。這是一個(gè)C#代碼的例子。

          public class Person : IDirty
          {
          protected string name;
          public virtual string Name
          {
          get { return name; }
          set
          {
          if (value != name)
          ((IDirty)this).Dirty = true;
          name = value;
          }
          }

          private bool dirty;
          bool IDirty.Dirty
          {
          get { return dirty; }
          set { dirty = value; }
          }
          }

          public interface IDirty
          {
          bool Dirty { get; set; }
          }
          清單 1

          如例中所見(jiàn),臟標(biāo)記在接口(IDirty)中定義,然后該接口由類顯式地實(shí)現(xiàn)。顯式接口實(shí)現(xiàn)(explicit interface implementation)是C#的一個(gè)良好的特性,它可以讓我們避免在類的默認(rèn)API中亂糟糟地堆滿基礎(chǔ)架構(gòu)的相關(guān)成員。

          這是有用的,例如在Visual Studio IDE中,除非將對(duì)象顯示地轉(zhuǎn)換為IDirty接口,否則Dirty標(biāo)記在代碼完成下拉菜單中是不可見(jiàn)的。事實(shí)上,可以從清單2中看出,為了訪問(wèn)Dirty屬性,對(duì)象首先必須轉(zhuǎn)換為IDirty接口。

          例子中的攔截代碼只有兩行(清單2)。不過(guò)那是因?yàn)榈侥壳盀橹梗覀兊睦又兄挥幸粋€(gè)屬性。如果我們有更多的屬性,我們將不得不在每個(gè)setter方法中重復(fù)這兩行代碼,并將比較對(duì)象改成相對(duì)應(yīng)的屬性。

          if (value != name)
          ((IDirty)this).Dirty = true;
          清單 2

          因此,寫代碼不會(huì)很難——并且我們的領(lǐng)域模型現(xiàn)在支持臟跟蹤,應(yīng)用中所有的組件只要用到對(duì)象模型都可以訪問(wèn)到臟跟蹤信息。從不需要隨時(shí)保留對(duì)象未經(jīng)修改版本的拷貝來(lái)說(shuō),這種做法還是節(jié)省資源和快速的。

          這種做法不利的一面是,你的領(lǐng)域模型類不再嚴(yán)格關(guān)注于商業(yè)業(yè)務(wù)。在領(lǐng)域模型類中忽然添加大量的基礎(chǔ)架構(gòu)代碼,使得實(shí)際的業(yè)務(wù)邏輯代碼變得更加難以理解,并且類本身變得更加不可靠、難以變更、難以維護(hù)。

          如果臟跟蹤是唯一必須插入到領(lǐng)域模型類中(或至少這樣做有好處)的基礎(chǔ)架構(gòu)功能,我們可以不用過(guò)于擔(dān)憂。但是不幸的是情況并非如此。這些功能的列表在不斷增加,表1中只列舉了一些例子:

          臟跟蹤 對(duì)象持有一個(gè)臟標(biāo)志,表示對(duì)象是否已經(jīng)被修改、但仍未保存。基于攔截和新成員的引入(臟標(biāo)記屬性及其getter、setter方法)。
          懶加載
          對(duì)象第一次被訪問(wèn)的時(shí)候才加載對(duì)象狀態(tài)。基于攔截和新成員的引入。
          初始值跟蹤 當(dāng)一個(gè)屬性被修改(但尚未保存)的時(shí)候,對(duì)象保留未修改值的拷貝。基于攔截和新成員的引入。
          反轉(zhuǎn)(Inverse)屬性管理 雙向關(guān)系中的一對(duì)屬性自動(dòng)保持同步。基于攔截。
          數(shù)據(jù)綁定 對(duì)象支持?jǐn)?shù)據(jù)綁定接口,從而在數(shù)據(jù)綁定情況下使用。基于新成員的引入(數(shù)據(jù)綁定接口的實(shí)現(xiàn))。
          復(fù)制 對(duì)象的變更分發(fā)到監(jiān)聽(tīng)系統(tǒng)。基于攔截。
          緩存 對(duì)象(或者至少是它們的狀態(tài),參見(jiàn)備忘錄模式[GoF設(shè)計(jì)模式])拷貝到本地存儲(chǔ)器中,隨后的請(qǐng)求可以在本地存儲(chǔ)器中查詢,而不用到遠(yuǎn)程數(shù)據(jù)源。基于攔截和新成員的引入。
          表 1

          這些功能——并且可能還有更多功能——都依賴于添加到領(lǐng)域類中的基礎(chǔ)架構(gòu)代碼,干凈、易于維護(hù)的領(lǐng)域模型忽然變得似乎遙遠(yuǎn)起來(lái)。

          我曾經(jīng)描述過(guò)這個(gè)問(wèn)題,其中,領(lǐng)域模型類中的代碼不斷增長(zhǎng),以至變得很難處理,就像肥領(lǐng)域模型反模式[Helander肥領(lǐng)域模型]。它和Martin Fowler描述的貧血領(lǐng)域模型反模式[Fowler貧血領(lǐng)域模型]正好相反,貧血領(lǐng)域模型反模式中領(lǐng)域模型類包含的邏輯過(guò)少。

          Fowler在他對(duì)貧血領(lǐng)域模型的描述中,解釋了將業(yè)務(wù)邏輯放在領(lǐng)域模型之外是一種錯(cuò)誤——實(shí)際上是一種反模式,因?yàn)椴荒艹浞掷妹嫦驅(qū)ο蟮母拍詈蜆?gòu)造。

          我同意他的論點(diǎn)——我甚至?xí)M(jìn)一步認(rèn)為這句話對(duì)基礎(chǔ)架構(gòu)代碼也一樣成立。基礎(chǔ)架構(gòu)代碼也可以通過(guò)分布在領(lǐng)域模型里,來(lái)利用面向?qū)ο蟮母拍詈蜆?gòu)造。如果一刀切地避免在領(lǐng)域模型中放置任何基礎(chǔ)架構(gòu)代碼,我們的代碼也將淪為貧血領(lǐng)域模型反模式相關(guān)問(wèn)題的犧牲品。

          不允許在領(lǐng)域模型中添加基礎(chǔ)架構(gòu)代碼——沒(méi)有充分利用面向?qū)ο蟆纱藥?lái)的限制我們已經(jīng)看過(guò):在應(yīng)用的不同組件之間共享基礎(chǔ)架構(gòu)功能變得更難,并且我們常常以低效率的“變通方法”而告終,比如用比較初始值來(lái)替代基于攔截的臟跟蹤。甚至還有一些功能在沒(méi)有攔截的情況下根本實(shí)現(xiàn)不了,如懶加載和反轉(zhuǎn)屬性管理。

          那么,解決方案是什么呢?如果我們把所有的基礎(chǔ)架構(gòu)代碼放置到領(lǐng)域模型類中,這些類就變得“肥胖不堪”——難于使用和維護(hù)。但是,如果我們把它放在領(lǐng)域模型類之外,我們又使其變?yōu)?#8220;貧血的”領(lǐng)域模型,基礎(chǔ)架構(gòu)代碼不能充分利用由面向?qū)ο蟮臐摿Α?br />

          我們似乎進(jìn)退維谷。更具體地說(shuō),在一個(gè)不易維護(hù)的肥領(lǐng)域模型和一堆與貧血領(lǐng)域模型共存的低效的變通辦法之間,我們進(jìn)退兩難。這顯然不是一個(gè)讓人特別愉快的境遇。現(xiàn)在該解決這個(gè)存在很久的問(wèn)題了:什么方法可以重構(gòu),讓我們走出這片混亂?

          使用一個(gè)公共的基礎(chǔ)架構(gòu)基類

          一種想法是嘗試將盡可能多的基礎(chǔ)架構(gòu)代碼放到一個(gè)公共的基類中去,領(lǐng)域模型類繼承該基類。然而,這個(gè)想法的主要問(wèn)題在于,它對(duì)引進(jìn)新的基礎(chǔ)架構(gòu)成員(像臟標(biāo)記)到領(lǐng)域模型類中有用的,但是它沒(méi)有為我們提供攔截,而攔截是我們想要支持的許多功能要求的。

          基類方法的另一個(gè)問(wèn)題是,當(dāng)需要調(diào)整功能的應(yīng)用方式的時(shí)候,它不能提供必要級(jí)別的粒度。如果不同的領(lǐng)域模型類有不同的基礎(chǔ)架構(gòu)需求,那么我們就會(huì)遇到問(wèn)題。我們也許能用一組不同的基類(也許互相繼承)來(lái)解決這些問(wèn)題,但是如果我們遇到單一繼承,比如C#和Java的情況,要是領(lǐng)域模型本身也想使用繼承,就沒(méi)法這么做了。

          為了明白這為什么會(huì)變成一個(gè)問(wèn)題,讓我們稍稍擴(kuò)展一下我們的例子,使其變得稍微“生動(dòng)”一些:假設(shè)我們有一個(gè)Person類,已經(jīng)能夠進(jìn)行臟跟蹤,還有一個(gè)Employee類同時(shí)具備臟跟蹤和懶加載能力。最后,Employee類需要繼承Person類。

          如果我們欣然把基礎(chǔ)架構(gòu)代碼放進(jìn)領(lǐng)域模型中,那么我們有一個(gè)像清單1的Person類,一個(gè)像清單3的Employee類。

          public class Employee : Person, ILazy
          {
          public override string Name
          {
          get
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //call the persistence component
          //and ask it to load the object
          //with data from the data source
          //(code omitted for brevity...)
          }
          return base.Name;
          }
          set
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }
          base.Name = value;
          }
          }

          private decimal salary;
          public decimal Salary
          {
          get
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }

          return salary;
          }
          set
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }

          if (value != salary)
          ((IDirty)this).Dirty = true;

          salary = value;
          }
          }

          private bool loaded;
          ///
          ///The Loaded property is "write-once" -
          /// after you have set it to true you can not set
          /// it to false again

          ///
          bool ILazy.Loaded
          {
          get { return loaded; }
          set
          {
          if (loaded)
          return;

          loaded = value;
          }
          }
          }
          清單 3

          正如你看到的,領(lǐng)域模型類中的實(shí)際業(yè)務(wù)內(nèi)容有被淹沒(méi)的危險(xiǎn),基礎(chǔ)代碼的膨脹已經(jīng)開(kāi)始悄悄蔓延了。肥領(lǐng)域模型就埋伏在前方,這種跡象應(yīng)該促動(dòng)我們嘗試一種不同的方法——即使它可能在前面階段更費(fèi)事一點(diǎn)兒。我們這么做是因?yàn)榫烷L(zhǎng)期而言,我們希望一個(gè)干凈的領(lǐng)域模型能夠收回在可維護(hù)性和可用性上的投資。

          因此,我們首先創(chuàng)建一個(gè)DirtyBase基類提供臟標(biāo)志,然后我們創(chuàng)建一個(gè)LazyBase提供一個(gè)加載標(biāo)志。使用多繼承的語(yǔ)言,我們可以讓Person類繼承DirtyBase類,Employee類繼承DirtyBase和LazyBase,如圖4描繪的一樣。


          圖 4

          但是如果我們要使用的語(yǔ)言不支持多繼承呢?好,我們能做的第一件事是讓LazyBase類繼承DirtyBase(見(jiàn)圖5)。那樣,我們?nèi)宰孍mployee類繼承LazyBase類,并仍有臟標(biāo)記和加載標(biāo)記。它可能不是最佳的解決方案(如果我們有一個(gè)對(duì)象需要懶加載,但是不需要臟跟蹤呢?),但是至少在這個(gè)例子中,它是一種選擇。


          圖 5

          不過(guò),這仍然給我們留了一個(gè)問(wèn)題,在像C#和Java這樣的語(yǔ)言里面,Employee類不能同時(shí)繼承Person類和LazyBase類。在這些語(yǔ)言中,剩下的辦法就是創(chuàng)建一個(gè)基類同時(shí)包括臟標(biāo)記和加載標(biāo)記(圖6),就讓Person類繼承一個(gè)多余的加載標(biāo)記。


          圖 6

          所以基類方法存在兩個(gè)問(wèn)題:它不能為許多功能(包括臟跟蹤和懶加載)提供所需的攔截,并且(至少在單繼承平臺(tái)上)它導(dǎo)致領(lǐng)域類繼承了一些它們并不必需的功能。

          使用基礎(chǔ)架構(gòu)代理子類

          幸運(yùn)的是,面向?qū)ο筇峁┝艘粋€(gè)絕好的方法來(lái)一石擊二鳥。為了同時(shí)提供攔截和粒度控制,我們所要做的就是創(chuàng)建繼承領(lǐng)域模型類的子類——每個(gè)(具體的)領(lǐng)域模型類都有一個(gè)新子類。

          這個(gè)子類能攔截對(duì)其基類成員的訪問(wèn),通過(guò)重寫(override)基類的成員來(lái)實(shí)現(xiàn),重寫的版本首先執(zhí)行攔截相關(guān)的活動(dòng),然后將執(zhí)行傳遞給基類的成員。

          為了避免在所有的子類中都創(chuàng)建公共的基礎(chǔ)架構(gòu)成員(比如臟標(biāo)記),仍然可以把它們放在所有領(lǐng)域類都會(huì)繼承的公共基類里面;而少數(shù)類的專屬功能,則可以將必要的成員放置在相關(guān)的子類中(圖7)。


          圖 7

          公共基類實(shí)現(xiàn)了所有領(lǐng)域模型類公有的基礎(chǔ)架構(gòu)成員。不是所有領(lǐng)域類公用的那些功能則使用接口。

          領(lǐng)域模型類脫離開(kāi)了基礎(chǔ)架構(gòu)代碼。

          代理子類重寫領(lǐng)域類成員來(lái)提供攔截。它們還實(shí)現(xiàn)了基礎(chǔ)架構(gòu)成員,這些成員不是所有領(lǐng)域類公有的。

          用子類的方式來(lái)提供攔截,本質(zhì)上是代理模式[GoF設(shè)計(jì)模式]的一種實(shí)現(xiàn)。還有一種變種是使用領(lǐng)域模型接口——一個(gè)領(lǐng)域模型類一個(gè)接口——接口能被代理類和領(lǐng)域類同時(shí)實(shí)現(xiàn)(圖8)。


          圖 8

          代理類在內(nèi)部屬性中持有一個(gè)領(lǐng)域類實(shí)例,它實(shí)現(xiàn)領(lǐng)域接口的時(shí)候把所有調(diào)用都轉(zhuǎn)發(fā)給背后的領(lǐng)域?qū)ο蟆_@實(shí)際上是在Gang of Four的《設(shè)計(jì)模式》[GoF設(shè)計(jì)模式]中描述的代理模式——只不過(guò)使用了子類來(lái)實(shí)現(xiàn)。

          使用子類的好處是,你不必非要?jiǎng)?chuàng)建一堆沒(méi)有其他用處的領(lǐng)域接口。同時(shí)還有一個(gè)好處,代理子類中的代碼能使用“this”關(guān)鍵字(VB.NET中是“Me”)調(diào)用繼承自領(lǐng)域類的代碼,而基于接口的代理中的代碼將不得不通過(guò)內(nèi)部屬性來(lái)引用領(lǐng)域?qū)ο蟆W宇愡€能訪問(wèn)受保護(hù)的成員,而基于接口的代理則要通過(guò)反射才能訪問(wèn)這些受保護(hù)的成員。

          使用基于接口的代理還有另一個(gè)缺點(diǎn),如果在領(lǐng)域類中有一個(gè)方法返回“this”的引用,調(diào)用代碼就會(huì)得到一個(gè)“未代理”的領(lǐng)域類實(shí)例,這意味著臟跟蹤和懶加載之類的功能在這個(gè)實(shí)例中是不可用的。

          由于這些問(wèn)題,我個(gè)人傾向于基于子類的代理方法,并且在整篇文章中我們會(huì)繼續(xù)探討這個(gè)方法。但是,請(qǐng)記住,在這篇文章中討論的所有技術(shù),也可以使用基于接口的代理方法來(lái)實(shí)現(xiàn)。

          POJO/POCO領(lǐng)域模型

          如果我們比較一下剛剛討論的基于代理模式的方法和我們先前的C#例子代碼,我們的領(lǐng)域類現(xiàn)在變得完全干凈、沒(méi)有任何基礎(chǔ)架構(gòu)代碼,就像在清單4前面部分中看到的一樣。所有的基礎(chǔ)架構(gòu)代碼已經(jīng)移到了基類和代理子類中,如清單4后面部分所示。

          //Domain Model Classes
          public class Person : DomainBase
          {
          protected string name;
          public virtual string Name
          {
          get { return name; }
          set { name = value; }
          }
          }

          public class Employee : Person
          {
          protected decimal salary;
          public virtual decimal Salary
          {
          get { return salary; }
          set { salary = value; }
          }
          }

          //Infrastructure Base Class
          public class DomainBase : IDirty
          {
          private bool dirty;
          bool IDirty.Dirty
          {
          get { return dirty; }
          set { dirty = value; }
          }
          }

          //Infrastructure Proxy Subclasses
          public class PersonProxy : Person
          {
          public override string Name
          {
          get { return base.Name; }
          set
          {
          if (value != this.name)
          ((IDirty)this).Dirty = true;
          base.Name = value;
          }
          }
          }

          public class EmployeeProxy : Employee, ILazy
          {
          public override string
          {
          get
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //call the persistence component
          //and ask it to load the object
          //with data from the data source
          //(code omitted for brevity...

          }
          return base.Name;
          }
          set
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }

          if (value != this.name)
          ((IDirty)this).Dirty = true;

          base.Name = value;
          }
          }

          public override decimal Salary
          {
          get
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }
          return base.Salary;
          }
          set
          {
          if (!loaded)
          {
          ((ILazy)this).Loaded = true;

          //perform lazy loading...
          //(omitted)

          }

          if (value != this.salary)
          ((IDirty)this).Dirty = true;

          base.Salary = value;
          }
          }

          private bool loaded;
          ///
          /// The Loaded property is "write-once" -
          /// after you have set it to true you can not set
          /// it to false again

          ///
          bool ILazy.Loaded
          {
          get { return loaded; }
          set
          {
          if (loaded)
          return;

          loaded = value;
          }
          }
          }
          清單 4

          通過(guò)與公共基類結(jié)合使用的代理模式,我們成功解決了我們看到的所有問(wèn)題,無(wú)論到目前為止嘲弄我們的這些問(wèn)題是來(lái)自于貧血領(lǐng)域模型反模式,還是肥領(lǐng)域模型反模式:

        1. 我們能將有關(guān)的基礎(chǔ)架構(gòu)代碼分布到領(lǐng)域模型中,使我們應(yīng)用中的所有部分都很容易訪問(wèn),并使它能用面向?qū)ο蠛透咝实姆椒▽?shí)現(xiàn)。
        2. 我們能夠把我們想要的基礎(chǔ)架構(gòu)代碼都分布到領(lǐng)域模型中,但避免了肥領(lǐng)域模型的結(jié)局。事實(shí)上,我們實(shí)際的領(lǐng)域模型類中的代碼,仍然能夠保持干凈,完全集中在它們需要關(guān)注的業(yè)務(wù)方面。
        3. 我們可以混合搭配,只增加每個(gè)領(lǐng)域模型類需要的基礎(chǔ)架構(gòu)代碼。
        4. 我們能構(gòu)建需要攔截支持的功能。
             

          應(yīng)該注意,按照POJO/POCO的嚴(yán)格定義,我們的領(lǐng)域模型類不應(yīng)該繼承基礎(chǔ)架構(gòu)基類。不過(guò),我們可以很容易改過(guò)來(lái),只要將所有的邏輯從公共基類移到代理子類中去,就可以得到一個(gè)完全的POJO/POCO領(lǐng)域模型。如果一定要滿足嚴(yán)格的POJO/POCO,我們的方法很容易滿足這項(xiàng)要求,只是舉手之勞。如果不強(qiáng)求,我們可以使用基類,此時(shí)領(lǐng)域模型類的代碼仍然完全不包含任何實(shí)際的基礎(chǔ)架構(gòu)代碼。

          因此到目前為止我們已經(jīng)整理出了一個(gè)架構(gòu),能讓我們把完全POJO/POCO的領(lǐng)域模型類和將基礎(chǔ)架構(gòu)代碼分布到領(lǐng)域模型中去的目標(biāo)結(jié)合在一起。如果我們的目標(biāo)僅僅是避免肥領(lǐng)域模型,而不強(qiáng)求符合POJO/POCO定義,那么我們就能走“半POJO/POCO”的路線,用一個(gè)公共基類來(lái)節(jié)省一些工作。

          使用抽象工廠模式

          這聽(tīng)起來(lái)很棒,對(duì)不對(duì)?你可能在想,用它是不是沒(méi)有任何問(wèn)題呢。軟件業(yè)的人都有些吹毛求疵,你大概已經(jīng)在疑心有那么一兩個(gè)棘手問(wèn)題該冒頭了吧。

          你是對(duì)的。有兩件值得關(guān)注的事情馬上就自己顯現(xiàn)出來(lái)了。第一個(gè)相當(dāng)輕微:為了讓子類能夠重寫領(lǐng)域模型類的成員,并提供攔截,所有的領(lǐng)域模型成員(或至少打算攔截的那些成員)必須是虛擬的。

          第二個(gè)問(wèn)題好像更為糟糕一些:既然你想讓你的程序使用代理子類的實(shí)例,你必須在應(yīng)用代碼中查找所有創(chuàng)建領(lǐng)域模型類實(shí)例的地方,將它們改成創(chuàng)建相應(yīng)的代理子類的實(shí)例。

          要修改已存在的應(yīng)用,聽(tīng)起來(lái)像個(gè)噩夢(mèng)吧?沒(méi)錯(cuò)。要想避免這種大規(guī)模的查找和替換操作,只有一條出路,就是從一開(kāi)始就避免使用“new”關(guān)鍵字來(lái)創(chuàng)建領(lǐng)域模型類的實(shí)例。

          避免"new"的傳統(tǒng)方法就是使用抽象工廠模式[GoF設(shè)計(jì)模式]。調(diào)用一個(gè)工廠類的Create()方法,而不是任由客戶代碼使用“new”來(lái)創(chuàng)建實(shí)例。Create()方法負(fù)責(zé)調(diào)用“new”,并且在返回新的實(shí)例之前會(huì)對(duì)其做一些額外的相關(guān)設(shè)置操作。

          如果你在調(diào)用領(lǐng)域模型的時(shí)候全都使用抽象工廠模式,那就最好不過(guò)了,接下來(lái)你只要在代碼中改一個(gè)地方——工廠類——將返回領(lǐng)域類實(shí)例改為返回代理子類(或者是基于接口的代理)的實(shí)例。

          這是使用抽象工廠模式來(lái)實(shí)例化所有的領(lǐng)域?qū)ο蟮囊粋€(gè)重要理由——至少使用像Java和C#這樣的語(yǔ)言時(shí)是這樣,這些語(yǔ)言不允許奇異特性,比如不能像C++一樣重載(overload)成員取用運(yùn)算子,不能像ObjectiveC一樣改變“new”關(guān)鍵字的行為,也不能像Ruby一樣在運(yùn)行時(shí)修改類。

          繼承反射

          對(duì)于代理子類的方法,還有一個(gè)問(wèn)題值得一提。雖然只是一種極端情況,但是它相當(dāng)隱蔽,如果你陷入這個(gè)問(wèn)題卻不知道是什么引起的,會(huì)被它狠狠咬上一口。

          如果你看一下圖9,你會(huì)發(fā)現(xiàn)Employee繼承Person類,這是應(yīng)該的:無(wú)論什么時(shí)候,如果一個(gè)方法期望Person對(duì)象,那么傳遞給它Employee對(duì)象應(yīng)該也能工作。此外,PersonProxy類繼承Person類。這也很好,因?yàn)檫@意味著將一個(gè)PersonProxy對(duì)象作為參數(shù)傳遞給期望Person對(duì)象的方法也是“合法的”。


          圖 9

          以同樣的方式,EmployeeProxy繼承Employee,意味著你能把一個(gè)EmployeeProxy對(duì)象作為參數(shù)傳遞給任何期望Employee對(duì)象的方法。最后,由于EmployeeProxy繼承Employee,Employee又繼承Person,這表明EmployeeProxy繼承Person。因此,任何期望Person對(duì)象的方法也可以接受EmployeeProxy對(duì)象。

          所有一切都符合我們的期望。當(dāng)我們讓抽象工廠開(kāi)始返回代理對(duì)象而不是領(lǐng)域模型類的簡(jiǎn)單實(shí)例時(shí),有一點(diǎn)是很重要的,我們希望客戶端代碼仍然繼續(xù)正常運(yùn)行,而無(wú)需重新考慮如何處理我們的類型層次。

          換句話說(shuō),如果我們?cè)瓉?lái)向一個(gè)期望Person對(duì)象的方法傳遞Employee對(duì)象給,當(dāng)我們忽然換成給它傳遞EmployeeProxy對(duì)象的時(shí)候,我們的代碼必須還能正常編譯(并且工作)。幸運(yùn)的是,由于EmployeeProxy繼承Employee,并且Employee繼承Person,所以沒(méi)有問(wèn)題。

          事實(shí)上,當(dāng)開(kāi)始使用代理來(lái)代替領(lǐng)域模型對(duì)象時(shí),一切都將如常繼續(xù)運(yùn)轉(zhuǎn),正如我們希望的。只不過(guò),有一個(gè)個(gè)非常微小的例外。

          圖9中的紅色繼承線表示EmployeeProxy不繼承PersonProxy。這什么時(shí)候會(huì)帶來(lái)問(wèn)題呢?

          好,考慮一下這樣一個(gè)方法,它接受兩個(gè)對(duì)象,并用反射來(lái)確定其中一種對(duì)象是否是另一種對(duì)象的子類。當(dāng)我們把一個(gè)Person對(duì)象和一個(gè)Employee對(duì)象傳遞給該方法時(shí),它會(huì)返回true。但是當(dāng)我們把一個(gè)PersonProxy對(duì)象和一個(gè)EmployeeProxy對(duì)象傳遞給該方法時(shí),突然它就返回false了。

          除非你有基于反射的、檢查繼承層次的代碼,不然應(yīng)該是安全的。但是萬(wàn)一你真的有這樣的代碼,這里先警告一下也沒(méi)壞處(你可以通過(guò)修改反射代碼來(lái)避開(kāi)這個(gè)問(wèn)題,讓它檢測(cè)代理類型,并一路向上直到找到一個(gè)非代理類型)。

          使用混入(Mixin)和攔截器類

          我們已經(jīng)向著可維護(hù)的領(lǐng)域模型架構(gòu)努力了很長(zhǎng)時(shí)間,讓它能容下一個(gè)有效率的、可訪問(wèn)的基礎(chǔ)架構(gòu)來(lái)實(shí)行運(yùn)行時(shí)領(lǐng)域模型管理。

          我們是否可以做得更多呢?

          讓我們開(kāi)始看一下那些只針對(duì)某些領(lǐng)域類、而不是全部領(lǐng)域類的功能——對(duì)于這些功能,我們需要將所需的基礎(chǔ)架構(gòu)成員放在代理子類中,而不是在公用的基類中。在我們的例子中,為懶加載功能設(shè)置的加載標(biāo)記將必須添加到需要支持懶加載的每個(gè)類的子類中。

          由于我們可能需要在多個(gè)子類中放相同的代碼,所以我們明顯會(huì)導(dǎo)致代碼重復(fù)。只有一個(gè)標(biāo)記的時(shí)候情況還好,但是對(duì)于需要多個(gè)屬性、甚至需要多個(gè)有復(fù)雜實(shí)現(xiàn)的方法的功能呢?

          在我們的例子中,加載標(biāo)記是一次寫入(write-once),所以在變成true之后不能再切換回來(lái)。因此,在setter方法中,我們有一些代碼來(lái)實(shí)施這個(gè)規(guī)則——在需要懶加載的每個(gè)領(lǐng)域模型類的子類中,我們都要在setter方法中重復(fù)的那些代碼。


          我們會(huì)希望創(chuàng)建一個(gè)可重用的類來(lái)包含這種邏輯。但是,我們已經(jīng)用了繼承(仍然假定我們工作在單繼承平臺(tái)上),那么這個(gè)懶加載類該怎樣被重用呢?

          一個(gè)很好的答案就是使用組合模式[GoF Design Patterns],而不是為此采用繼承。使用這種模式,EmployeeProxy子類將包含一個(gè)內(nèi)部屬性來(lái)引用可重用的懶加載類的實(shí)例(圖10)。


          圖 10

          這種可重用的類常常被稱為混入(mixin),它反映了這樣一個(gè)事實(shí),即實(shí)現(xiàn)被加入到類型中,而不是成為繼承層次的一部分。使用混入的作用是,讓單繼承語(yǔ)言(甚至是在只支持接口繼承的平臺(tái)上,比如COM+)也能夠獲得類似多重繼承的效果。

          有一點(diǎn)可以補(bǔ)充說(shuō)明一下,使用我們這種組合方式,以混入的形式存在的行為和狀態(tài)能動(dòng)態(tài)地被加到目標(biāo)類中(使用依賴注入),而不是靜態(tài)地綁定到目標(biāo)上去(如果行為是繼承下來(lái)的就會(huì)如此)。盡管在這篇文章中我們不會(huì)更多關(guān)注于如何利用它的潛能,但是它為支持一些非常靈活和動(dòng)態(tài)的場(chǎng)景提供了非常大的空間。

          通過(guò)將子類引入的成員分離到一個(gè)可重用的混入類,我們向一個(gè)真正模塊化、一致的、內(nèi)聚的架構(gòu)邁進(jìn)了一大步,這個(gè)架構(gòu)還是低耦合的、高度代碼可重用的。我們能向這個(gè)方向再進(jìn)一步嗎?

          那么,接下來(lái)要做的事就是把攔截代碼從子類中分離出來(lái),并放到可重用的類里面。這些攔截器類會(huì)像混入一樣包含在代理子類里面,如圖11.


          圖 11

          Person、Employee和DomainBase與代碼的上一個(gè)版本保持不變,但是PersonProxy和EmployeeProxy發(fā)生了變化,并且我們引入了兩個(gè)新的攔截器類和一個(gè)新的混入類。清單5顯示了我們進(jìn)行這些重構(gòu)(不包括未修改的類)之后代碼的樣子。

          //These proxy subclasses contain only boilerplate
          //code now. All actual logic has been refactored
          //out into mixin and interceptor classes.

          public class PersonProxy : Person
          {
          private DirtyInterceptor
          dirtyInterceptor = new DirtyInterceptor();

          public override string Name
          {
          get { return base.Name; }
          set
          {
          dirtyInterceptor.OnPropertySet(this, this.name, value);
          base.Name = value;
          }
          }
          }

          public class EmployeeProxy : Employee, ILazy
          {
          //This mixin contains the implementation
          //of the ILazy interface
          .
          private ILazy lazyMixin = new LazyMixin();

          private LazyInterceptor lazyInterceptor = new LazyInterceptor();
          private DirtyInterceptor dirtyInterceptor = new DirtyInterceptor();

          public override string Name
          {
          get
          {
          lazyInterceptor.OnPropertyGetSet(this, "Name");
          return base.Name;
          }
          set
          {
          lazyInterceptor.OnPropertyGetSet(this, "Name");
          dirtyInterceptor.OnPropertySet(this, this.name, value);
          base.Name = value;
          }
          }

          public override decimal Salary
          {
          get
          {
          lazyInterceptor.OnPropertyGetSet(this, "Salary");
          return base.Salary;
          }
          set
          {
          lazyInterceptor.OnPropertyGetSet(this, "Salary");
          dirtyInterceptor.OnPropertySet(this, this.name, value);
          base.Salary = value;
          }
          }

          //The ILazy interface is implemented
          //by forwarding the calls to the mixin,
          //which contains the actual implementation
          .
          bool ILazy.Loaded
          {
          get { return lazyMixin.Loaded; }
          set { lazyMixin.Loaded = value; }
          }
          }

          //The following mixin and interceptor classes
          //contain all the actual infrastructural logic
          //associated with the dirty tracking and
          //the lazy loading features.

          public class LazyMixin : ILazy
          {
          private bool loaded;
          ///
          /// The Loaded property is "write-once" -
          /// after you have set it to true you can not set
          /// it to false again

          ///
          bool ILazy.Loaded
          {
          get { return loaded; }
          set
          {
          if (loaded)
          return;

          loaded = value;
          }
          }
          }

          public class DirtyInterceptor
          {
          public void OnPropertySet(
          object obj,
          object oldValue,
          object newValue)
          {
          if (!oldValue.Equals(newValue))
          {
          IDirty dirty = obj as IDirty;
          if (dirty != null)
          dirty.Dirty = true;
          }
          }
          }

          public class LazyInterceptor
          {
          public void OnPropertyGetSet(object obj)
          {
          ILazy lazy = obj as ILazy;
          if (lazy != null)
          {
          if (!lazy.Loaded)
          {
          lazy.Loaded = true;

          //perform lazy loading...
          //(omitted)

          }
          }
          }
          }
          清單 5

          通過(guò)重構(gòu),最終所有實(shí)際的基礎(chǔ)架構(gòu)邏輯都被放進(jìn)了混入和攔截器類,代理子類變得很苗條,變成專注于轉(zhuǎn)發(fā)請(qǐng)求到攔截器和混入的輕量級(jí)類。事實(shí)上,在這一點(diǎn)上,代理子類里面除了由很容易被代碼生成工具生成的樣板代碼之外,再?zèng)]其它什么內(nèi)容。

          一種選擇是在設(shè)計(jì)時(shí)生成代理子類的代碼,但是像C#和Java這樣的語(yǔ)言能讓你在運(yùn)行時(shí)生成、編譯、執(zhí)行代碼。因此,我們也可以選擇在運(yùn)行時(shí)生成代理子類,這種做法就是俗稱的運(yùn)行時(shí)子類化(runtime subclassing)。

          花一點(diǎn)兒時(shí)間回頭想想我們走了多遠(yuǎn)。在第一步,我們將肥領(lǐng)域模型重構(gòu)為帶有代理子類的POJO/POCO領(lǐng)域模型(還可選擇增加一個(gè)基類),同時(shí)基礎(chǔ)架構(gòu)代碼仍然分布在領(lǐng)域模型中。在重構(gòu)的第二步,所有實(shí)際的基礎(chǔ)架構(gòu)邏輯從代理子類分離了出來(lái),并加入到可重用的混入和攔截器類,在代理子類中只留下了樣板代碼。

          在最后一步,我們轉(zhuǎn)向使用運(yùn)行時(shí)代碼生成器來(lái)為樣板代理子類生成全部代碼。就這樣我們一路跋涉到了面向方面編程的疆界。

          使用面向方面編程

          很長(zhǎng)一段時(shí)間,我對(duì)AOP的大肆宣傳都感到很新奇,每次我嘗試研究它是怎么一回事的時(shí)候,就會(huì)完全迷惑——這主要因?yàn)锳OP社區(qū)堅(jiān)持使用他們哪種奇怪的術(shù)語(yǔ)。而且常常有一些(不是全部!)AOP倡導(dǎo)者似乎在故意讓這個(gè)領(lǐng)域看起來(lái)那么的不同,讓人興奮并覺(jué)得膽怯。

          因此,如果說(shuō)讀到這里,實(shí)際上你已經(jīng)掌握了面向方面編程的所有主要概念,你可能會(huì)感到驚訝——不但如此,而且你已經(jīng)看到了AOP框架在背后是如何運(yùn)作的,從頭到腳。

          面向方面編程使用了“方面”這個(gè)核心概念,方面簡(jiǎn)單來(lái)說(shuō)是一組引介(introductions,即混入)和通知(advice,即攔截器)。方面(攔截器和混入)可以通過(guò)運(yùn)行時(shí)子類化(runtime subclassing)的方式(以及其他技術(shù))在運(yùn)行時(shí)應(yīng)用于現(xiàn)有的類。

          正如你看到的,你已經(jīng)理解了大部分怪異但重要的AOP術(shù)語(yǔ)。被大量用來(lái)描述方面對(duì)什么有益的術(shù)語(yǔ)橫切(crosscutting)關(guān)注點(diǎn),簡(jiǎn)單意思就是你有一個(gè)能應(yīng)用于大多數(shù)(不一定屬于同一個(gè)繼承樹的)類的功能——例如我們的臟跟蹤和懶加載功能,這些功能就是橫切跨領(lǐng)域模型的關(guān)注點(diǎn)。

          我們至今還未真正涵蓋、唯一真正重要的AOP術(shù)語(yǔ)是連接點(diǎn)(join point)和切點(diǎn)(pointcutting)。連接點(diǎn)是目標(biāo)類中的一個(gè)地方,你可以在那里應(yīng)用攔截器或混入。通常,攔截器所關(guān)心的連接點(diǎn)是方法,而混入所關(guān)心的連接點(diǎn)則是類本身。簡(jiǎn)單地說(shuō),連接點(diǎn)就是你能加入方面的點(diǎn)。

          為了確定哪個(gè)方面應(yīng)該被應(yīng)用到哪個(gè)連接點(diǎn),你可以使用切點(diǎn)語(yǔ)言。這個(gè)語(yǔ)言可以很簡(jiǎn)單,可以是描述性的語(yǔ)言,比如你可能只是在一個(gè)xml文件中命名方面、命名連接點(diǎn)組合;但是也有復(fù)雜的可以使用正則表達(dá)式、甚至完整的領(lǐng)域特定語(yǔ)言(Domain Specific Languages)來(lái)進(jìn)行更高級(jí)的“切點(diǎn)“。

          到目前為止,我們還尚未關(guān)注切點(diǎn),因?yàn)槲覀円呀?jīng)在我們需要的地方手工應(yīng)用了我們的方面(我們的混入和攔截器)。但是,如果我們要想走完最后的一步,為應(yīng)用我們的方面而使用AOP框架,我們還是需要考慮這個(gè)問(wèn)題。

          但在我們做之前,先讓我們停下來(lái)思索一下這個(gè)計(jì)劃。在我們前面已經(jīng)完成那么多重構(gòu)之后,現(xiàn)在加入AOP框架步子其實(shí)并不大。它僅僅是很小的一步,因?yàn)槲覀円呀?jīng)有效地重構(gòu)了我們的應(yīng)用,使其使用攔截器(通知)和混入(介紹)形式的方面。

          事實(shí)上,我會(huì)認(rèn)為我們實(shí)質(zhì)上已經(jīng)在利用AOP了——我們只是還沒(méi)有用AOP框架來(lái)為我們自動(dòng)化那些例行公事的部分。但是我們利用著來(lái)自AOP的所有基本的概念(攔截和混入),并且我們用一種可重用的方式處理橫切關(guān)注點(diǎn)。這聽(tīng)起來(lái)像極了AOP的定義,所以AOP框架能幫助我們自動(dòng)化完成之前的一些手工工作是一點(diǎn)兒也不會(huì)令人感到意外的。

          目前攔截器和混入類中的代碼只要輕微地修改一下就是方面了——它已經(jīng)被重構(gòu)為一個(gè)泛化的、可重用的形式,這正是方面應(yīng)該具有的形式。我們需要AOP框架做的只是以某種方式幫助我們將方面應(yīng)用到領(lǐng)域模型類中。如前所述,AOP框架有許多方式可以做到這一點(diǎn),其中一種方式是在運(yùn)行時(shí)生成代理子類。

          如果用的是運(yùn)行時(shí)子類化框架,生成的代理子類代碼看起來(lái)會(huì)和我們例子中代理子類的樣板代碼非常近似。換句話說(shuō),它很容易生成。如果你認(rèn)為你可以寫一個(gè)代碼生成器來(lái)完成任務(wù),那么你就在實(shí)現(xiàn)你自己的AOP框架引擎的半路上了。

          重構(gòu)到方面

          在我們最后的重構(gòu)中,我們將看一下,如果我們使用一個(gè)AOP框架來(lái)應(yīng)用我們的攔截器和混入,那么我們的例子代碼會(huì)怎樣。在這個(gè)例子中,我將使用NAspect,這是Roger Johansson和我合作完成的一個(gè)針對(duì).NET的開(kāi)源AOP框架。NAspect使用運(yùn)行時(shí)子類來(lái)運(yùn)用方面。

          讀到這里,至今你應(yīng)該不難理解NAspect執(zhí)行的“花招”——它簡(jiǎn)單地生成包含樣板代碼的代理子類,樣板代碼需要轉(zhuǎn)發(fā)請(qǐng)求到混入和攔截器,并且它使用標(biāo)準(zhǔn)的、面向?qū)ο蟮摹㈩I(lǐng)域類成員的重寫,來(lái)提供攔截。

          很多人把AOP和最糟糕的巫術(shù)聯(lián)系在一起。奇怪的術(shù)語(yǔ)已經(jīng)令人怯步,開(kāi)發(fā)人員在領(lǐng)域類代碼中預(yù)見(jiàn)不到的事情忽然并“毫無(wú)察覺(jué)”地發(fā)生在攔截器中,令很多人遭受打擊,深深地感到不安。

          當(dāng)然,具體了解這些術(shù)語(yǔ)是什么意思會(huì)有幫助。能理解至少一種運(yùn)用方面的方法也是有幫助的,至少能讓你看到這不是什么魔術(shù)——只是幾個(gè)良好的、成熟的、眾所周知的、面向?qū)ο蟮脑O(shè)計(jì)模式在發(fā)揮作用。

          AOP就是用我們熟知的面向?qū)ο髞?lái)對(duì)付橫切關(guān)注點(diǎn)。AOP框架只是讓你能夠更舒服地運(yùn)用方面,不用自己手寫許多代理子類。

          所以,現(xiàn)在我們不會(huì)再被AOP給嚇到了,讓我們看看我們?nèi)绾问褂盟鼇?lái)使我們的應(yīng)用向終點(diǎn)線邁出最后一步。

          在這個(gè)例子里,我們要做的第一件事情是包含一個(gè)對(duì)NAspect框架程序集的引用。準(zhǔn)備好之后,我們就可以開(kāi)始創(chuàng)建我們的方面、確定我們的切點(diǎn)。我們也可以直接刪除PersonProxy和EmployeeProxy類——從現(xiàn)在開(kāi)始,NAspect會(huì)為我們生成這些類。

          如果我們?cè)敢猓覀兩踔量梢詣h除DomainBase基類——沒(méi)有必要使用基類來(lái)應(yīng)用公用于所有基類的混入,我們也可以使用AOP框架來(lái)做這些。這意味著,通過(guò)使用AOP框架,我們甚至可以無(wú)需任何額外工作就能滿足POJO/POCO的嚴(yán)格要求。

          為了除去基類,我們只需要把功能移到混入中去。就我們的情況而言,我們僅需要再創(chuàng)建一個(gè)混入——DirtyMixin,它持有先前放在基類中的臟標(biāo)記。

          LazyMixin類中的代碼完全保持不變,所以這里不再列出。實(shí)際上,我們真正需要去做的是創(chuàng)建新的DirtyMixin類,并稍微修改兩個(gè)攔截器中的代碼,讓它們使用NAspect傳進(jìn)來(lái)的描述被攔截方法的數(shù)據(jù)結(jié)構(gòu)。

          我們還需要修改我們的工廠類,以便它使用NAspect來(lái)為我們創(chuàng)建代理子類。這假定我們已經(jīng)在應(yīng)用中的所有地方都轉(zhuǎn)為使用抽象工廠模式。如果我們還沒(méi)有這樣做,現(xiàn)在我們一定要汲取教訓(xùn),因?yàn)闆](méi)有抽象工廠模式,我們將不得不進(jìn)行另一項(xiàng)龐大的搜索、替換操作,仔細(xì)檢查代碼中每一個(gè)代理子類被實(shí)例化的地方,改為調(diào)用NAspect運(yùn)行時(shí)子類化引擎。

          重構(gòu)后的代碼如清單6所示,準(zhǔn)備好使用NAspect。

          public class DirtyMixin : IDirty
          {
          private bool dirty;
          bool IDirty.Dirty
          {
          get { return dirty; }
          set { dirty = value; }
          }
          }

          public class DirtyInterceptor : IAroundInterceptor
          {
          public object HandleCall(MethodInvocation call)
          {
          IDirty dirty = call.Target as IDirty;

          if (dirty != null)
          {
          //Extract the new value from the call object
          object newValue = call.Parameters[0].Value;

          //Extract the current value using reflection
          object value = call.Target.GetType().GetProperty(
          call.Method.Name.Substring(4)).GetValue(
          call.Target, null);

          //Mark as dirty if the new value is
          //different from the old value

          if (!value.Equals(newValue))
          dirty.Dirty = true;
          }

          return call.Proceed();
          }
          }

          public class LazyInterceptor : IAroundInterceptor
          {
          public object HandleCall(MethodInvocation call)
          {
          ILazy lazy = call.Target as ILazy;

          if (lazy != null)
          {
          if (!lazy.Loaded)
          {
          lazy.Loaded = true;

          //perform lazy loading...
          //(omitted)

          }
          }

          return call.Proceed();
          }
          }

          public class Factory
          {
          public static IEngine engine = ApplicationContext.Configure();

          public static Domain.Person CreatePerson()
          { return engine.CreateProxyPerson>();
          }

          public static Domain.Employee CreateEmployee()
          {
          return engine.CreateProxyEmployee>();
          }
          }
          清單 6

          為了知道你的方面要應(yīng)用到哪個(gè)類,NAspect在程序的配置文件中使用一個(gè)xml配置段(也有其它選擇)。現(xiàn)在我們可以在xml文件中定義方面,指出混入和攔截器類的類型,以及我們想應(yīng)用的類型。大體上,我們準(zhǔn)備好了嘗試一下我們的AOP應(yīng)用。

          不過(guò),仍然有一點(diǎn)讓許多開(kāi)發(fā)人員對(duì)使用AOP猶豫不決,甚至當(dāng)他們理解了AOP的術(shù)語(yǔ)和理論,那就是由于切點(diǎn)體系定義得不完善而帶來(lái)的脆弱性。

          雖然我們通過(guò)一些巧妙的正則表達(dá)式來(lái)篩選出打算應(yīng)用方面的類和成員,但風(fēng)險(xiǎn)是當(dāng)領(lǐng)域模型變化的時(shí)候,你可能會(huì)忘了更新切點(diǎn)定義,以致正則表達(dá)式忽然篩選到了不應(yīng)該應(yīng)用方面的類。就是這類問(wèn)題給AOP帶來(lái)了巫術(shù)的壞名聲。

          處理切點(diǎn)不明確的一個(gè)方式是創(chuàng)建自定義的.NET屬性(Attribute)(Java中的注釋(annotations))。用自定義屬性裝飾領(lǐng)域模型,然后使用屬性做為切點(diǎn)目標(biāo),這樣就可以避免使用正則表達(dá)式之類的小花招。方面只應(yīng)用于我們決定該應(yīng)用的地方,通過(guò)在那里放置屬性。

          因此根據(jù)我們的情況,我們繼續(xù)創(chuàng)建兩個(gè)自定義屬性——LazyAttribute和DirtyAttribute——接下來(lái)我們使用它們來(lái)裝飾我們的類(清單7)。

          public class DirtyAttribute : Attribute
          {
          }

          public class LazyAttribute : Attribute
          {
          }

          [Dirty]
          public class Person : DomainBase
          {
          protected string name;
          public virtual string Name
          {
          get { return name; }
          [Dirty]
          set { name = value; }
          }
          }

          [Dirty]
          [Lazy]

          public class Employee : Person
          {
          protected decimal salary;
          public virtual decimal Salary
          {
          [Lazy]
          get { return salary; }
          [Dirty]
          [Lazy]

          set { salary = value; }
          }

          public override string Name
          {
          [Lazy]
          get { return name; }
          [Dirty]
          [Lazy]

          set { name = value; }
          }
          }
          清單 7

          我們接著在應(yīng)用的配置文件中定義我們的切點(diǎn)(清單8),讓方面以我們自定義的屬性為目標(biāo)。





          name="naspect"
          type="Puzzle.NAspect.Framework.Configuration.NAspectConfigurationHandler, Puzzle.NAspect.Framework.NET2"/>







          name="DirtyAspect"
          target-attribute="InfoQ.AspectsOfDMM.Attributes.DirtyAttribute, InfoQ.AspectsOfDMM" >

          target-attribute="InfoQ.AspectsOfDMM.Attributes.DirtyAttribute, InfoQ.AspectsOfDMM" >

          type="InfoQ.AspectsOfDMM.Aspects.DirtyInterceptor, InfoQ.AspectsOfDMM" />







          name="LazyAspect"
          target-attribute="InfoQ.AspectsOfDMM.Attributes.LazyAttribute, InfoQ.AspectsOfDMM" >

          target-attribute="InfoQ.AspectsOfDMM.Attributes.LazyAttribute, InfoQ.AspectsOfDMM" >

          type="InfoQ.AspectsOfDMM.Aspects.LazyInterceptor, InfoQ.AspectsOfDMM" />









          現(xiàn)在,我們最后測(cè)試一下我們的AOP應(yīng)用。當(dāng)我們運(yùn)行應(yīng)用時(shí),NAspect會(huì)在運(yùn)行時(shí)為我們生成代理子類,并在子類中插入樣板代碼來(lái)將所有的請(qǐng)求轉(zhuǎn)發(fā)到攔截器和混入類。

          程序最后的架構(gòu)可參看圖12。注意,這個(gè)版本和先前非AOP版本之間的唯一實(shí)際差別是,兩個(gè)代理類(使用虛線邊框標(biāo)識(shí))將由AOP框架生成,而之前我們必須手工編碼。

          與圖11比較,我們多了一個(gè)DirtyTrackerMixin類和一個(gè)新的IDirtyTracker接口,來(lái)代替DomainBase基類,但這些東西哪怕我們不使用AOP,只要決定不使用公共基礎(chǔ)架構(gòu)基類(以滿足更嚴(yán)格的POJO/POCO需求)就是要有的。換句話說(shuō),如果我們不想使用一個(gè)公共基類,不管我們是否使用AOP框架,我們最終都會(huì)得到一樣的架構(gòu)(圖12所示)。

          圖 12

          切點(diǎn)選項(xiàng)

          當(dāng)你添加新的領(lǐng)域模型類時(shí),要想使它們能夠懶加載和臟跟蹤,只需要用自定義屬性裝飾它們,就會(huì)”變魔術(shù)一樣地“獲得相應(yīng)功能了。

          用屬性/注釋來(lái)作為切點(diǎn)目標(biāo),很快你就會(huì)注意到,由于每項(xiàng)功能對(duì)應(yīng)一個(gè)屬性,很快領(lǐng)域模型中每個(gè)成員的屬性就太多了。因此,為了減少屬性的數(shù)量,你可能想找到它們之間的抽象性。

          如果你適應(yīng)基于正則表達(dá)式的切點(diǎn)方法,并且你覺(jué)得屬性令領(lǐng)域模型變得雜亂,使得保持領(lǐng)域模型與基礎(chǔ)架構(gòu)關(guān)注點(diǎn)完全無(wú)關(guān)的目標(biāo)受到了損害,那么另一種選擇是,你可以只配置你的xml配置文件來(lái)匹配相關(guān)的目標(biāo)類,并且不需要用自定義屬性裝飾你的領(lǐng)域類。

          另外,NAspect的xml切點(diǎn)語(yǔ)言(像許多其它的AOP框架一樣)允許你簡(jiǎn)單地提供一個(gè)完整的類型清單,方面會(huì)運(yùn)用于列出的類型,讓你不用去找到一個(gè)剛好只匹配正確類的正則表達(dá)式。這使配置文件變得更長(zhǎng),但是能讓你的領(lǐng)域模型保持完全干凈。

          在使用切點(diǎn)來(lái)動(dòng)態(tài)運(yùn)用方面的地方使用AOP框架,還有一個(gè)好處就是,當(dāng)方面被用在不同的用例中時(shí),把不同的方面應(yīng)用于相同的類中會(huì)變得很容易。如果一個(gè)用例要求某個(gè)領(lǐng)域類懶加載,而另一個(gè)用例不需要,那么只有第一個(gè)用例需要使用應(yīng)用懶加載方面的配置文件。

          這個(gè)動(dòng)態(tài)的方面應(yīng)用可以非常強(qiáng)大,比如在測(cè)試場(chǎng)景可以額外應(yīng)用一些測(cè)試用的方面,提供mocking之類的功能。

          使用面向方面編程的結(jié)論

          我們是否可以在領(lǐng)域模型中放置任何基礎(chǔ)架構(gòu)代碼,這是我們開(kāi)始時(shí)的問(wèn)題。我們看到,這絕對(duì)可取,因?yàn)樗钤S多功能得以更有效率地實(shí)現(xiàn),但是可能使我們的領(lǐng)域模型變得“肥胖”。

          為了解決這個(gè)問(wèn)題,我們重構(gòu)了我們的肥領(lǐng)域模型——首先是把基礎(chǔ)架構(gòu)代碼移到代理子類和一個(gè)基類中,然后將實(shí)際的邏輯從代理子類中移出、移到混入和攔截器類中。這使我們的領(lǐng)域模型變得更漂亮,但是在代理子類中最終卻有很多樣板代碼需要寫。為了解決這個(gè)問(wèn)題,我們轉(zhuǎn)向了面向方面編程。

          結(jié)果發(fā)現(xiàn)邁向AOP框架的步子原來(lái)是如此之小——事實(shí)上應(yīng)用的架構(gòu)并沒(méi)有任何改變——以至它大概使一些讀者感到驚奇,這些讀者甚至有熟悉的感覺(jué),這種感覺(jué)就像當(dāng)你第一次理解一個(gè)設(shè)計(jì)模式時(shí),你意識(shí)到它就是你已經(jīng)用了很多次的東西,只是不知道它有一個(gè)這樣的名字。

          你很可能在許多應(yīng)用中有效地使用過(guò)AOP,但并沒(méi)認(rèn)識(shí)到它是AOP,并且好像沒(méi)有使用過(guò)AOP框架來(lái)幫助你自動(dòng)化地生成部分樣板代碼。

          我們現(xiàn)在在這篇文章的結(jié)尾處,很希望在這個(gè)地方你會(huì)同意我的看法:

        5. AOP不像它看上去的那么難以理解和令人迷惑,并且使用AOP框架是很容易的。
        6. 不管你是否使用AOP框架,使用AOP的概念和建模技術(shù),對(duì)處理你應(yīng)用基礎(chǔ)架構(gòu)中的橫切領(lǐng)域模型管理關(guān)注點(diǎn)來(lái)說(shuō),仍然是一個(gè)非常好的方法。
             

          它實(shí)際上就是一個(gè)通過(guò)使用良好的、成熟的面向?qū)ο竽J剑€(wěn)步提高應(yīng)用架構(gòu)的重構(gòu)問(wèn)題。這樣做的時(shí)候,不要害怕走向一個(gè)使用代理來(lái)運(yùn)用攔截器和混入的,可稱為面向方面的應(yīng)用架構(gòu)。

          不管你是否進(jìn)一步讓一個(gè)AOP框架來(lái)幫助你應(yīng)用方面,這都不會(huì)真正影響你是否在進(jìn)行AOP。因?yàn)椋绻闶琼樦@篇文章討論的路線來(lái)重構(gòu)你的應(yīng)用的,你就是在進(jìn)行AOP——不管有沒(méi)有一個(gè)框架的幫助。

          摘要

          通過(guò)使用像代理模式、抽象工廠模式、組合模式這些傳統(tǒng)的、知名的面向?qū)ο笤O(shè)計(jì)模式來(lái)重構(gòu)我們的肥領(lǐng)域模型,我們得到了一個(gè)極好地結(jié)合了精益領(lǐng)域模型和基礎(chǔ)架構(gòu)的應(yīng)用架構(gòu),基礎(chǔ)架構(gòu)充分利用了面向?qū)ο蟮母拍詈蜆?gòu)造。這樣,我們巧妙地避免陷入肥領(lǐng)域模型和貧血領(lǐng)域模型反模式的陷阱。

          使用這篇文章中討論的模式,得到了一個(gè)可以被形容為面向方面的應(yīng)用架構(gòu)。這是因?yàn)槲覀冏罱K用混入和攔截器類解決了橫切關(guān)注點(diǎn),混入和攔截器則精密地與面向方面編程中的介紹和通知匹配了起來(lái)。

          如果你想親自體驗(yàn)使用AOP是不是真的那么輕松愉快,而且不介意使用不是親手鍵入的代碼,你可以下載本文隨附的Visual Studio 2005工程的所有代碼。

          總之,在這篇文章中,我嘗試去展示面向方面的概念和工具是怎樣提供一個(gè)偉大的方式去看待和運(yùn)用許多領(lǐng)域模型管理關(guān)注點(diǎn),以及它們是如何大大減輕了肥領(lǐng)域模型反模式的風(fēng)險(xiǎn)。

          關(guān)于作者

          Mats Helander是Avanade(荷蘭)的一位資深軟件開(kāi)發(fā)咨詢師。業(yè)余時(shí)間,他和Roger Johansson一起開(kāi)發(fā)了免費(fèi)的Puzzle.NET套件,Puzzle.NET是一個(gè)開(kāi)源框架和工具。Puzzle.NET包括NPersist、ObjectMapper(對(duì)象/關(guān)系映射)、NAspect(面向方面編程)、NFactory(依賴注入)、以及NPath(內(nèi)存對(duì)象查詢)。

          Mats Helander的博客——http://www.matshelander.com/wordpress
          Puzzle.NET——http://www.puzzleframework.com
          Avanade(荷蘭)——http://www.avanade.com/nl/


        7. jwebee

          我的個(gè)人網(wǎng)站
          posted on 2008-03-11 19:01 周行 閱讀(573) 評(píng)論(1)  編輯  收藏 所屬分類: IT技術(shù)

          FeedBack:
          # re: 領(lǐng)域模型管理與AOP
          2011-10-28 21:35 | 楊Coder
          infoQ真是個(gè)好網(wǎng)站,還有不少好文章!謝謝你引導(dǎo)了我。  回復(fù)  更多評(píng)論
            
          Java-Android-jwebee
          主站蜘蛛池模板: 青岛市| 蕉岭县| 凤庆县| 麟游县| 孟州市| 桂东县| 绵竹市| 大冶市| 安陆市| 漳平市| 富川| 嵩明县| 白水县| 平舆县| 灵丘县| 赤水市| 辽阳市| 三原县| 泰兴市| 德阳市| 冀州市| 沙洋县| 庄浪县| 锡林浩特市| 定州市| 道孚县| 韩城市| 阿尔山市| 南阳市| 赤壁市| 鄱阳县| 丰都县| 蓝山县| 江门市| 玉林市| 岳阳县| 曲沃县| 库伦旗| 类乌齐县| 武陟县| 唐山市|