閱讀233 返回首頁    go 小米 go 小米6


角色對象模式

意圖

單個對象透過不同的角色對象來滿足不同客戶的不同需求。每一個角色對象針對不同的客戶內容來扮演其角色。對象能夠動態的管理其角色集合。角色作為獨立的對象是的不同的內容能夠簡單的被分離開來,係統間的配置也變得容易。

譯注:為了行文的流暢性及內容意思的準確性,盡量貼近原文使用英文單詞標記特定內容, 如Customer表示客戶,Client表示客戶端,Component表示組件等。因為有各種圖例說明,所以在圖例說明時,使用原題中的英文單詞對應圖中內容。有時也中英文交叉使用。因為網頁顯示的問題,中文黑體使用綠色標注。

 

動機

麵向對象係統是基於一個關鍵抽象集。每一個關鍵抽象是為一個基於抽象狀態和行為類的模型化。這種情況,適合於較小的程序。但是,一旦我們通過整合不同程序的方式擴展我們的程序,我們不得不在我們的關鍵抽象中處理好不同的客戶端對其特定內容視圖(Context View)的需求。

假設我們開發一個對銀行投資部門的支持軟件。其中一個關鍵抽象為Customer(客戶)這個概念。因此,我們的設計模型應該包含一個Customer類。這個類接口能夠管理客戶的私人信息,如姓名、地址、存款、支出等。

現在假設銀行的放貸部門也需要軟件支持。現在看起來我們設計的客戶類不足以完成一個借款者的角色。很明顯,需要提供進一步的實現狀態及操作來管理客戶的借貸賬戶、貸記、證券。

在同一個類中整合不同的環境視圖導致關鍵抽象擴展為臃腫的接口。這樣的接口不僅理解起來很困難,而且維護性差。此外,不恰當的變動係統若不能恰當地處理,甚至可能觸發大量的重編譯。針對特定的客戶的類接口部分的變動,很可能影響甚至損害其他子係統或者程序中的客戶端

一個簡單的解決方式是擴展Customer類,添加BorrowerInvestor子類,他們分別處理借貸者部分和投資者的部分。如果從對象的身份視角來看,子類化意味著不同子類的兩個對象是不一樣的客戶。一個客戶可以使用兩個身份來扮演各自借貸者以及投資者角色。身份證明(Identify)僅能通過額外的機製來模擬(譯者:因借貸者和投資者其實是一個人,隻需要一個身份證)。如果兩個對象的身份是一致的,他們所繼承的屬性始終需要一致性檢查。然而,在多態搜索中此類問題就無法避免的,例如,當我們想構建一個係統中客戶列表時,同樣的Customer對象將會重複出現,除非我們消除這種重複。

角色對象(Role Object)模式建議把一個對象的特定內容視圖拆分成角色對象角色對象能過動態的從核心對象(Core Object)中添加和移除。我們把這種由一個核心及角色對象構成的相關的組合對象結構稱作主語(Subject)。一個主語通常扮演不同的角色並且同一個角色也很可能是在不同的主語中。比如兩個客戶可以獨立扮演借貸者以及投資者就角色。這兩個角色都是在單個的客戶對象主語中完成工作。


圖1:在銀行環境中的客戶體係

一個關鍵抽象(key abstraction),比如Customer,被定義為要一個抽象超類,僅看做一個純粹的接口,裏麵沒有定義任何實現狀態。Customer類具體說明其處理客戶地址、賬號以及定義了最小的協議來管理角色。CustomerCore子類實現了Customer接口。

CustomerRole為特定內容的角色提供超類,並且提供Customer接口支持。CustomerRole類是一個抽象類並且不能被實例化。CustomerRole的具體子類,例如借貸者以及投資者,定義並實現了針對特定角色的接口。它們僅是是能夠在運行時實例化的子類。借貸者的特定內容視圖由放貸部門定義,它定了額外的才做來管理客戶的貸記以及證券。類似的,投資者類增加具體的操作來表達投資部門對於客戶的視圖。


圖2:角色對象模式中一個對象的示意圖

一個客戶端,比如借貸程序,通過Customer接口類或者與CustomerCore類的對象一起工作,也可能是與CustomerRole具體子類一起工作。假設一個借貸程序通過Customer接口了解到一個特定的客戶實例,借貸程序想知道這個Customer對象是否是借貸者角色,它調用hasRole()方法,來確認此對象是否是滿足借貸者的角色定義。通常使用字符串來命名角色,如果Customer對象能夠扮演所命名的“Borrower”借貸者角色,借貸程序將要求它返回一個相關聯的對象引用,借貸程序現在就可以使用這個對象引用來完成借貸者相關的操作。


可用性

使用角色對象模式,如果

*   你想處理在不同內容中的一個關鍵抽象,但是你不願意讓所有針對特定內容的接口都在同一個接口類中。
*   你想動態的處理可用角色,那樣你能夠在需要的時候添加或者刪除。即使是在運行時,而不是把他們固定在編譯時。
*   你想讓擴展的過程變得透明,並且需要保證相關對象體係中的對象身份邏輯。
*   你想保持角色、客戶端相互獨立,這樣的話對於角色的變化不會影響到客戶端,因為客戶端對角色不感興趣。

不要使用這種模式在

*   如果潛在的角色具有強烈的相對獨立性。

這些就是使用角色的設計變體。有一個關於使用這些變體的指導Fowler97


結構

下列圖片為角色對象模式的示意圖


圖3:角色對象模式結構示意圖


組成部分

  • Component(Customer)(客戶)組件
    • 通過定義其接口模型化了一個特殊的關鍵抽象
    • 申明了角色對象需要用的添加、移除、測試、查詢協議。一個客戶端提供了一個對於具體角色子類的具體需求,它使用字符喘傳來定義。
  • ComponentCore(CustomerCore)
    • 實現了一個Component接口,包括角色管理協議;
    • 創建了具體角色實例;
    • 管理其角色對象;
  • ComponentRole(CustomerRole)
    • 存儲一個被裝飾的ComponentCore的引用;
    • 通過遞呈請求到它的core上,實現了一個Component接口
  • ConcreteRole(Investor, Borrower)
    • 模型化並且實現了Component接口的特點內容擴展;
    • 使用ComponentCore作為參數可以被實例化;

協作

核心對象core object 以及角色對象role object的協作如下:

  • ComponentRole遞呈求到它的ComponentCore對象上;
  • ComponentCore實例化並且管理具體角色;

一個客戶端client角色對象核心對象交互如下:

  • 客戶端使用角色擴展核心對象。這樣,它能夠使用具體的對象來描述它期望的角色;
  • 客戶端任何時候都是在指定的方式與core交互,完成其工作。如果客戶端需要某個角色,從核心對象中請求這個角色。如果這個core對象正好扮演了被請求的角色,它就返回自己的給客戶端;
  • 如果核心對象不是扮演說請求的角色,一個錯誤被拋出。核心對象永遠不會獨立地創建角色對象;

結果

角色對象模式有如下優點及重要性:

  • 關鍵抽象定義簡潔。Component接口很好的關注了被模型化的關鍵抽象的本質的狀態及行為,他不會因為特定的角色接口的擴展變的臃腫;
  • 角色演化很簡單並且角色之間時相互獨立的。擴展一個Component接口很容易,因為沒有必要改變ComponentCore類。一個具體角色類讓你可以添加新的角色以及角色實現並且能夠保護關鍵抽象自身;
  • 角色對象可以動態的添加或移除。一個角色對象在運行時簡單地添加或者刪除與核心對象。因此,在給定環境中需要的對象是可以實時創建的。
  • 程序之間解耦。通過從角色中準確的分離出Component接口,基於角色擴展的程序需要緊耦合的地方減少了。使用Component接口以及特定的具體角色類的程序A(ClientA)不需要知道被用於*程序*B(ClientB)的具體角色類;
  • 類組合爆炸通過使用多繼承得到避免。此模式避免了類組合爆炸,因為它通過多繼承來組合不同的角色到一個類中。

角色對象模式的缺點或不利條件:

  • 客戶端變的更加複雜。相比使用Component接口,通過對象的ConcreteRole類來與對象一起工作,具有較小的代碼量。客戶端會在具體問題中檢查對象扮演的角色,如果通過,客戶端需要為這個角色進行查詢,如果沒通過,client針對其特定需要負責擴展核心對象,來使得核心對象可以扮演需要的角色;
  • 不同角色間的維護約束變的困難。因為由那些變化又相互依賴的對象組成的主語的維護性約束以為為了維護全部主語的的一致性的需求就變得困難。在現實部分我們將討論幾個產生的問題。
  • 在角色中的角色不能被類型係統強製執行。你可能會想通過角色結合的的方式來排除角色到核心對象上,或者確定的角色依賴於其他的的一些角色。但是在角色對象模式中,你不能依賴類型係統為你強製執行約束,你不得不使用運行時來檢查。
  • 維護對象身份變得困難核心對象以及來至於概念單元的角色實例應該都有其自身的概念身份。技術上的對象身份可以通過編程語言來檢查其身份(技術上的對象身份檢查通過比較對象引用識別),但是Component接口上概念對象身份的檢查需增加額外的操作。這裏可以通過實現核心對象的引用比較來識別。


實現

實現角色對象模式需要解決兩個關鍵問題:為角色透明的擴展關鍵抽象以及動態管理這些角色。對於透明擴展,我們可以使用裝飾模式Gamma+95。對於創建及管理角色,我們運用產品交易者模式Bäumer+97。因此,角色對象模式結合兩種著名的模式來完成新的語義。

  1. 提供接口一致。因為我們想角色對象的使用是透明的,即核心對象能夠被使用,但是他們需要通過一個通用接口。注意,從模型化的觀點看,一個角色類被當做它核心的特殊化(如投資者是一個客戶)。裝飾模式告訴我們如何完成。首先,我們考慮一個針對所有對象的通用接口。此接口能夠動態的添加角色到所有對象上去,並且結構圖中的Component類提供這個類,並且在裝飾模式中與此Component類協作。對於所有特定內容角色可以擴展這個Component的功能,我們介紹的抽象超類ComponentRole就是在裝飾模式中的協作類。ComponentRole通過遞呈操作請求到核心對象上來實現Component接口。因此,RoleCore透明的包裝了。ConcreteRole類必須繼承於ComponentRole。他們相互協作如裝飾模式中的具體裝飾者。
  2. 隱藏角色對象的創建過程。在運行時角色實例被用於裝飾核心對象。一個關鍵的問題是ConcreteRole實例是如何創建及添加到核心對象上去的。注意,ConcreteRole不是由客戶端創建,角色的創建過程是由ComponentCore初始化的,因此避免了角色對象可能自我存在的可能(如角色對象獨立於核心對象而存在,是不允許的)。這也保證了客戶端不知道角色對象如何被實例化。
  3. 角色類從核心類中解耦。角色的創建及管理是普通行為。換句話說,使用新的並且無法預知的角色來擴展ComponentCore而又不改變ComponentCore這種做法是不現實的。因此,創建以及管理的過程必須獨立於具體的角色類,ComponentCore代碼不必靜態引用這些角色對象。使用規範的對象specification objects可以完成這個目標。請求一個對象,客戶端傳遞一個具體的對象到核心上,最簡單的解決方案是使用類型名作為規範。核心對象返回滿足規範的角色對象。相同的規範對象被用於創建。為了完成這個目標,使用產品交易者模式來獲得支持。角色對象商人為那些相關的創建者對象維持一個規範對象的容器,例如,類對象,原型或者模範者。當客戶的想要創建一個新角色時,它傳遞一個規範對象到核心,核心把這個創建委托給角色對象商人。
  4. 選擇合適的規範對象。在許多例子中,使用類型名作為規範對象十分有效的,但是一些時候,使用較複雜的規範機製可能會更好:假如你已經模型化了人Person這個該概念作為核心對象類。一些人可能是雇員Employee,因此有一個Employee類。以為存在不同的雇員類型,你必須是使用雇員接口類並且模型化具體的角色作為雇員子類,例如銷售,開發者,經理等。當客戶端需要關於Person的工資的信息時,他將會從核心對象中請求角色“Employee”。這就不可能使用類型名類完成需要的工作,因為具體角色對象是銷售人員等類型名。在一些情況中,你可以使用Type Objects 作為規範方式。核心對象在隨後取回請求的角色對象通過評估子類或者超類之間的類型關係。
  5. 管理角色對象。現在讓核心對象管理角色,Component接口聲明角色管理協議,此協議包含了添加、刪除、測試、查詢角色對象的操作。為了支持角色管理協議,核心對象維護一個映射了對象規範與具體角色實例的字典。無論何時一個角色對象被添加到核心對象上,新的角色對象就與其角色規範被注冊在角色字典中。請注意,一個核心對象管理它的角色對象是通過ComponentRole類型引用,因此排除了扮演角色的ComponentCore實例!因為核心擁有角色,它必須照顧他們。比如,當需要的刪除的時候,它能夠刪除他們。
  6. 維護一致的核心以及角色對象的狀態。對於核心對象或者角色對象的改變可能需要更新更深入的一些對象。看一個例子,考慮改變“Person”這個核心對象的名稱,此時也是借貸者收到一個新名字的任何時候,在借貸者中的必須有一個標記來指明借貸者的名字的改變。這個標記為係統指明了名字的改變必須報備於全局機構中,這個全局機構包含並傳遞了諸如借貸者的信譽度之類的信息。在銀行中,通知是必須步驟。有幾個可能的解決方案來確保這個約束,但是所有的都是有代價的。通常依賴使用硬編碼的方式。在下麵一個模塊中我們將會討論一個精心組織的解決方案。
  7. 使用產權及觀察者來維護角色屬性約束。如果一個狀態的整合以為相互依賴變得複雜,(部分)核心對象的實現狀態可以使用一個所有權列表property list來代表,或者叫做可變狀態所有權列表使用key/value鍵值對來代表屬性名稱與屬性值。它通常使用字典實現,字典就是映射了屬性名與屬性值。角色對象可以注冊感興趣的屬性(此屬性為核心對象的部分狀態),並且在它感興趣的這些狀態改變操作發生時被通知。為了完成這個過程,改變了屬性的每一個對象必須通知核心對象它的這個改變,如此,它能夠通知以來的角色對象它的這種改變。所有權列表通常是一個不好的方法,因為違背了封裝原則,暴露了內部狀態。屬性名的改變可能需要所有的角色類適當地的變動。另外,糟糕的代碼也可能導致負麵影響。然後,當小心地處理後,這些問題可以避免。可能最好的例子是抽象語法樹中被裝飾節點的分布使用以及在許多編譯器或者軟件開發環境中關鍵抽象。
  8. 維護概念上的身份。角色對象模式讓你管理核心類及其角色類作為一個獨一的整合了狀態的概念上的對象。因此,對於客戶端來說,它能夠清晰的分辨出技術上的兩個對象其實是一個概念對象。客戶端必須使用由Component接口提供的具體身份比較操作。通常,此操作使用直接比較兩個核心對象的引用。
  9. 維護角色中的約束。在不同角色間,可能存在一些約束。一個通用的事例是角色B需要角色A所扮演的對象。例如,客戶和借貸者都是的角色,存在一個客戶角色對象是是前提條件,因為允許人中借貸者角色完整角色扮演。這個就是角色級約束。一個角色對象的扮演角色B的能力必須是在能夠扮演角色A的前提下,或者扮演角色A的對象A已經存在。如果沒有角色A,角色B不能完成扮演動作。這個約束來至於特定的係統域。一般來說,角色B不僅僅依賴於角色A而且可以把角色A作為一個特殊狀態。在複雜的事例中,你將不可避免的使用約束解決係統。幸運的時,在實踐中,不會進一步增加其複雜度,並且許多務實的解決方案是行之有效的。典型的案例是使用兩階段提交協議,在移除一個角色對象前,需要首先征詢所有角色是否同意完成這個請求。
  10. 維護因為遞歸角色對象模式而產生的約束。角色級role-level的約束中,許多問題都能通過遞歸角色對象來解決。如果角色A是一些類角色B、C、D等的前提條件,角色A必須作為這些角色的一個關鍵抽象被定義。借貸者以及投資可以使用客戶視角來觀察,客戶以及擔保人可以使用人的視角來觀察。因此作為一個擔保人,不需要一個客戶,也不需要被模型化為一個客戶,下麵的圖表現了一個遞歸角色對象模式的運用。


圖5:角色對象模式的遞歸運用

在運行時,這導致角色鏈以及核心對象。下列圖片描述了這個種過程:


圖6:角色上繼續添加角色的動態對象示例圖

角色級別約束由角色對象借貸者或者投資者執行。而不是進入更高的層級-客戶層級。因此,模型化的客戶作為一個關鍵抽象是相對於人這個更一般的關鍵抽象來完成其特定的角色扮演約束的。


簡單代碼

下列C++代碼描述了一個在動機裏麵討論的如何實現客戶的例子。我們假設存在一稱作Customer的Component。

01 class CustomerRole;
02  class Customer {
03  public:
04  // 客戶規範操作
05  virtual list<Account *> getAccounts() = 0;
06  // 角色管理
07  virtual CustomerRole * getRole(String aSpec) = 0;
08  virtual CustomerRole * addRole(String aSpec) = 0;
09  virtual CustomerRole * removeRole(String aSpec) = 0;
10  virtual CustomerRole * hasRole(String aSpec) = 0;
11 };

CustomerCore的實現像這樣子:

01 class CustomerCore : public Customer {
02  public:
03  CustomerRole * getRole(String aSpec)
04  {
05  return roles[aSpec];
06  };
07  CustomerRole * addRole(String aSpec)
08  {
09  CustomerRole * role = NULL;
10  if ((role = getRole(aSpec)) == NULL)
11  {
12  if(role = CustomerRole :: createFor(aSpec, this)) roles[Spec] = role;
13  }
14  return role;
15  };
16  list<Account *> getAccounts() { ... };
17  private:
18  map<String, CustomerRole *> roles;
19 };

角色規範的中使用字符串來帶代表具體的角色類。使用字典映射角色規範以及角色對象。

下一步,我們定義客戶的子類叫做CustomerRole類,我們將對他子類化來獲得具體的角色。CustomerRole裝飾CustomerCore類通過引用core實例變量。對已每一個Customer接口每一個操作,CustomerRole遞呈請求給core。注意,core的實例變量被CustomerCore分型。因此,為了保證客戶角色不被用於core對象,角色規範以及相對應個的可以創建角色實例化的創建者對象之間使用查找表。詳細的如何實現一個管理創建者請看Bäumer+97

01 class CustomerRole : public Customer {
02  public:
03 list<Account *> getAccounts() { return core->getAccounts() }; CustomerRole * addRole(String aSpec) { return core->addRole(aS pec); };
04  static CustomerRole * createFor(String aSpec, CustomerCore * aCore)
05  {
06  CustomerRole * newRole = NULL;
07  if (newRole = lookup(aSpec)->create()) newRole->core = aCore;
08  return newRole;
09 };
10  private:
11  CustomerCore * core;
12 };

CustomerRole子類規範了各種角色。例如,類Borrower添加了證券以及貸記的操作。子類不應複寫繼承的角色管理操作。

1 class Borrower : public CustomerRole {
2  public:
3 list<Security *> getSecurities() { return securities; };
4  private:
5  list<Security *> securities;
6 };

注意,client在他們在角色實例中調用規範的角色操作之前,必須向下轉換由core組件返回角色引用。

1 Customer * aCoustomer = Database :: load(“Tom Jones”); Borrower * aBorrower = NULL;
2 if (aBorrower = dynamic_cast<Borrower *> aCustomer->getRole( “Borrower”)) {
3       // access securities
4          list<Security *> securities = aBorrower->getSecurities();
5    };

已存在的係統

GEBOS係列麵向對象的銀行項目就使用此模式Bäumer+97擴展。它為一係列的銀行商業應用提供支持,包括出納、借貸、投資部門以及自我服務及賬戶管理。GEBOS係統基於通用的商業領域的分層模型化銀行的核心概念。具體的工作場合運用程序使用角色對象模式擴展這些核心的概念。

Riehle+95aRiehle+95bTool-And-Material框架通過複製、粘貼、多繼承、裝飾者以及包裝者探索了角色模型設計空間來取得角色對象模式相同的效果。這些變體也在Fowler97中有介紹。

當前得Geo係統在Ubilab發展,Ubilab是瑞士聯合銀行信息技術研究實驗室,它使用角色對象模式作為一個角色變體的實現做一個程序第一級實體。

Kristensen與Østerbye報告了在編程語言中為角色使用裝飾者模式Kristensen+96。然後,他們沒有說明創建及管理角色的細節。

我們使用特定領域例子,及其角色,來達到達到上述目的。為了表現它擁有的模式,此例子是能夠提供共性的東西。因為的抽象需要許多的內容,也有許多不同的角色需要人來扮演。Schoenfeld96討論了幾個例子,例如人及其角色在基於文件為中心的商業處理過程。我們選擇人及其角色在銀行商務係統的中的扮演客戶的內容。當然另外一個例子是人及其角色官僚體係中收入管理問題。

一個不相關的角色對象模式使用是在抽象語法樹(AST)中的裝飾節點。在大部分的開發環境中AST是基本的抽象。它們在許多的不同工具中被使用及被考慮,例如語法製導編輯器、符號瀏覽、交叉引用,編譯支持、依賴分析變化影響。每一個工具需要注釋AST節點使用規定的信息,它之針對整棵樹的某一個細小的部分。角色對象模式通過特定的工具針對特定的的節點是很有效的。Mitsui+93討論了使用這種模式在C++編程語言中內容中的的情況,它針對了特定的內容用,當然也討論了更多一般性問題。


涉及到的模式

擴展對象模式Gamma97處理了同樣的問題:一個組件通過擴展的對象完場擴展,使用以下方式:統計特定內容的需求。但是這些模式沒有討論組件以及組價角色對象之間如何完成處理的,這個反而是角色對象模式中的關鍵內容。另外,擴展對象模式僅僅觸及到了擴展對象(角色對象)的創建及管理問題。我們整合了裝飾模式以及商品交易者模式作為角色對象模式的一部分。

擴展對象模式在Zhao+97Schoenfeld96被使用。Zhao和Foster討論了角色對象作為擴展對象,但是他們顯然沒有包裝核心對象。他麼的關鍵例子是(角色)標記作為的一個透明軟件係統中的關鍵點。Schoenfeld選擇如我們同樣的例子,人及其角色,也使用擴展對象模式而不是使用裝飾類透明的包裝core

Post模式Fowler96描述了此模式一個有趣的變體,類似於擴展對象模式,它描述了在特定程序中核心對象的內容的職責。然而,Post對象的存在是獨立與core的,並且不需要core來分配。

鳴謝

我們感謝Shepherd Ari Schoenfeld對於本文的幫助與提高。

最後更新:2017-05-23 11:02:14

  上一篇:go  深入理解Java內存模型(三)——順序一致性
  下一篇:go  深入理解java內存模型係列文章