天天看點

對象的角色 對象的角色

對象的角色

若要獲得良好的對象設計,就必須對職責進行合理的配置設定。每個對象承擔的職責不能太多,也不能太少,恰如其分即可。職責配置設定如樂譜中對音符的組織,高明的音樂家總是能讓不同的音符放在合理的位置,奏成悅耳的心曲,表達音樂家的内心感情。然而,即使設計師明确職責配置設定的重要性,在面臨紛亂複雜的需求時,常常被亂花迷了眼,或者無法識别正确的職責,又或者顧此失彼,将職責放錯了位置,變成了對職責混亂的塗鴉。

要識别職責,進而合理配置設定職責,有許多秘訣,或雲“技巧”。不過,将對象的角色作為職責配置設定的開始,不失為一個好的起點。角色是對象的身份,若以拟人化的方式思考對象世界,就可以設想:究竟是怎樣的身份,需得承擔怎樣的職責,才會與其身份相當,不至于亂了規矩。紅樓夢中,劉外婆進了大觀園,出盡了洋相,就是因為身份失當;又可以想想倘若林黛玉像尤三姐那般愛恨分明,也不至于見花落淚,惹人愛憐了。故而在配置設定職責時,我們能首先明确對象的角色,即可将思想帶入到這一角色中,設身處地,推斷這一角色可以或者必須承擔哪些職責。

在Object Design:Roles, Responsibility, and Collaborations一書中,将對象的角色分為了五種,分别為資訊持有者、構造者、服務提供者、協調者和控制者。這是一種設計上的抽象劃分,它迥異于你針對具體業務識别的角色。然而,在分辨職責并企圖對其進行配置設定時,确乎有一定的參考價值。以此為基礎,在進行軟體設計時,可以思考你要設計的對象,究竟屬于哪一種角色。

資訊持有者

首先來看資訊持有者。顧名思義,這種角色的對象必然持有相關的資訊。不過,俯瞰對象世界,除了某些特殊的行為對象而言,大多數對象都必然持有相關的資訊。是以,這裡的角色劃定,其主要意圖在于讓設計者明确,與資訊相關的行為,如處理資訊的方式,資訊變化造成的影響等,都應首先考慮是否由該資訊的持有者來承擔。這近似于Larman在Applying UML and Patterns一書中提到的“資訊專家模式”。

在面向對象設計時,許多設計者往往忽略了這一點,這也是設計出“貧血對象”的錯誤根源。其實,在進行面向對象設計時,需得設計者有一顆“拟人”的想象力,将你需要設計的一個個對象看做是能夠思考具有智力判斷的人物就好了。那麼,在人類生活中,将專業的事情交給專業的人去做,不是理所當然的嗎?何謂“專業”?不就是他或者她擁有與此領域有關的知識嗎?映射到OO世界,所謂“知識”就是對象所擁有的資訊,而所謂“專業的事情”則相當于操作資訊的行為;故而首當其沖地設計結果就是将操作資訊的行為與擁有資訊的對象有機的結合起來,這也是所謂的“資料與行為應該封裝在一起”原則。當然,這裡的行為當以“角色”的角度觀察之,反映到代碼層面,則可以是接口。袁英傑的文章小類,大對象庶幾闡述了這一設計思想。

讓我們還是回到資訊持有者這個話題上。

例如,我們需要設計一個Web伺服器,它提供了一個對象

HttpProcessor

,能夠接收由

HttpConnector

發送來的Socket請求,對Request進行處理,并在處理後将相關資訊放入Response中。請求和響應被封裝在對應的

HttpRequest

HttpResponse

對象中。在處理請求和響應資訊時,需要對Socket消息進行處理,并為Request和Response對象設定相關屬性。

對于消息的解析工作,這裡存在兩個設計選擇。其一是放在

HttpProcessor

對象中,看起來(從命名看)它才是消息的專項處理者;其二則是将對Request和Response的解析工作分别放到各自的

HttpRequest

HttpResponse

對象中。

該如何選擇?我們遵循資訊持有者的設計要求,答案不言而喻。如下圖所示:

對象的角色 對象的角色

遵循資訊持有者的特征,

HttpProcessor

HttpRequest

HttpResponse

之間的權責變得更加清晰。此外,這一設計方式還有利于改善性能。某些Http請求解析可能牽涉到系統開銷較大的字元串操作,而解析的内容并不是在一開始就需要使用。将解析職責轉移到

HttpRequest

中,就使得

HttpProcessor

的process()操作可以快速完成,并将相關請求資料流高效地塞到

HttpRequest

對象中。隻有真正需要相關請求資訊時,才向HttpRequest對象發出解析的請求消息。這種方式頗像是對象的Lazy Load。

構造者

構造者角色主要承擔對象的建立,以及對複合對象的組裝。如果熟悉設計模式,可以發現構造者角色基本上涵蓋了構造型模式的意圖。例如建立對象,組合對象,以及選擇對象構造的方式。此外,還有一種特殊的構造者角色對象,即它可能具有雙重角色,一方面作為構造者角色,另一方面也作為構造者所建立出來的産品。這種雙重角色的構造者角色,常常會形成一條構造鍊。

例如,在JMS中,若要獲得

Queue

對象,就可能由

ConnectionFactory

對象建立出

Connection

對象,則通過該對象建立

Session

對象,最後由

Session

對象建立的

Queue

。如下圖所示:

對象的角色 對象的角色

為何需要構造者角色?畢竟對象自身可以擁有構造函數,以提供給調用者完成對象的建立。通常情況下,之是以引入構造者角色,主要是為了:

  • 應對建立的變化,例如Factory Method模式或Abstract Factory模式;
  • 隐藏對象建立的複雜邏輯,例如Static Factory模式或Builder模式;
  • 控制對象建立的時機或數量,例如Singleton模式。

服務提供者

關于服務提供者,一個重要認識是:它能提供具有“業務價值”的行為。所謂“業務價值”,即一定是實作業務邏輯中不可缺少的,且相對獨立完整的功能。這就意味着,擔任服務提供者角色的對象,常常是一個職責完備的,實作了某個業務關注點的可重用對象。此外,業務價值是有層次之分的。在最外層,可能意味着一個完整的業務流程,此時服務對象暴露給用戶端的,是一個封裝了服務實作細節的對象(可能是接口);而為了實作該外層服務,又可能在整個實作中,需要更為細粒度的内層服務對象提供各個實作步驟的支撐。

站在架構角度思考,這種對服務提供者的分層,可能正好對應DDD分層架構中的Application Service與Domain Service。若設計為Application Service,需得遵循DDD的語義,對外而言,它确實代表了整體的業務邏輯,對内,則不過扮演了Facade的角色,是對多個Domain Service的一種封裝而已。從某種意義上講,這樣的Application Service更像是後面要說的“協調者”角色。但由于它具有非常明确的業務含義,我更傾向于将它視為服務提供者。

例如,系統需要定期根據使用者送出的資料生成稅務報表。假設它的業務流程是讀取報表資料後,對資料流進行處理,并以HTML格式呈現,最後生成PDF檔案。對外而言,稅務報表的生成是一個完整的服務,用戶端的調用者無需了解這個服務的實作細節。因而對外可以定義

TaxReportGenerator

服務對象,它對外接收給定的報表名,結果則是生成報表的PDF檔案。顯然,它具有非常重要的業務價值。

接下來考慮該對象的内部實作。由于報表生成需要執行多個業務步驟,如果将這些職責均交給

TaxReportGenerator

來處理,無疑會導緻該對象承擔過重的職責。此外,呈現HTML格式與PDF檔案生成對于稅務報表生成而言,是整個業務流程中的一環;但從單個職責而言,無疑它們又是獨立的。可以設想,倘若系統還有其他業務功能需要生成PDF檔案,又或者需要按照規定形式呈現為HTML頁面,将這些職責封裝到單獨的職責中,就可能很好地支援重用。從“業務價值”的角度看,它們無疑同樣具備了服務提供者的能力。整個

TaxReportGenerator

對象的内部協作如下圖所示:

對象的角色 對象的角色

協調者

協調者有些像設計模式的Mediator模式,即用于協調對象職責的協作,又或者負責轉發或委派請求。協調者是孜孜不倦助人為樂的居委會大媽,既善于也樂于協調鄰裡之間的糾紛。除了可以以中間人的身份協調對象,進而簡化對象之間的協作,降低複雜的依賴關系外,協調者還能很好地隐藏這些互動細節。這就使得調用者變得簡單,還能讓這種關系協調的實作集中在一處,即使将來協調關系發生了變化,也可以做到僅修改一處,即可應對變化。從這一點來看,似乎協調者又展現了Facade模式。

正如前面所言,DDD中的Application Service頗為接近協調者角色。然而,我之是以不希望将這二者混為一談,還是從業務(領域)的角度來思考問題。我認為協調者的引入僅僅是為了改善設計品質的,它本身(無論是對外,還是對内)并不具有業務價值,這是至為關鍵的一點差別。

在一個大型複雜系統中,提供了許多Web Service。不同的Web Service可能需要支援不同的消費者,而這些服務的部署位置也可能并不相同。消費者需要準确定位到相關服務,然後通過一些相對複雜的實作邏輯,完成對服務的調用。這類邏輯就牽涉到消費者、服務以及服務調用與服務位置之間的協作。如果沒有合适的對象去封裝,既可能導緻細節暴露,增加複雜度,也無法做到有效重用。一旦協作的邏輯發生變化,可能還會導緻這種變化蔓延到系統的各個地方。這時,就是展現協調者價值所在了。

在這個場景下,我們可以引入

ServiceLocator

對象來負責整個協調邏輯,它能夠根據消費者請求的服務類型定位服務,然後找到服務端口,發送服務請求。下圖展示了這種協調邏輯的具體做法,注意不同的服務消費者都經由相同的ServiceLocator角色完成了不同的服務調用:

對象的角色 對象的角色

控制者

看到控制者,我們或許會想到MVC模式的Controller。确乎它們具有相似的特性,即用于控制多個對象之間的互動,甚至是驅動對象。我們可以将這裡所謂的控制者角色,看做是Controller的外延,即它具有更加寬泛的職責意義。凡是需要控制角色互動,并具有一定控制邏輯的對象,均可視為控制者角色。注意,控制者角色與協調者角色的差別,前者多少具有一定的管理特征,被控制的對象在級别上要低于控制者角色;後者則展現出一種平等的層級關系。簡而言之,前者是政府官員,後者是居委會大媽。

當然,在設計時,有時很難泾渭分明地界定這二者。這就好似用例中的包含(include)與擴充(extend)關系,許多設計者還在孜孜以求,絞盡腦汁地要分辨出二者的不同,以保證正确地運用用例關系,求得完美的設計,孰知早有用例專家(Corkburn)給出忠言,不必一定區分包含與擴充,因為它對用例的編寫不會産生直接的重大影響。參考此例,我也希望設計師不必鑽牛角尖,隻需明白此兩種角色,其本質還在于隐藏對象的協作或互動細節,降低複雜度,保證重用以及對變化的應對。

在軟體設計中,我們經常遇到控制者角色。一個常見的例子是由控制者角色承擔判斷邏輯,根據不同的請求,經由不同的分支調用不同的對象。例如在一個系統中,我們需要對頁面的内容合法性進行驗證。不同的内容對驗證的要求不盡相同。一個簡單的判斷是看内容是否隻需要對頁面頭進行驗證,如果為非,則需要對整個頁面進行驗證。在設計時,我們引入了

ValidationProcessor

來控制這種驗證邏輯。站在調用者的角度,驗證的事情交給ValidationProcessor去處理就好,管它是否僅是一個控制樞紐,真正的驗證卻是它要委派的對象呢?

對象的角色 對象的角色

當然,在這裡的

ContentController

同樣屬于控制者角色,事實就是MVC模式中的Controller,用于控制

Content

ContentView

之間的互動。而

ValidatorProcessor

與MVC風馬牛不相及,但在本文的語義中,仍可以看做是控制者角色。

結語

我通常将這五種對象角色劃分為兩大類:領域對象與設計對象。資訊持有者和服務提供者都屬于領域對象,因為它或者持有了真正的領域邏輯,或者它在接口層面上代表了用戶端需要使用的領域邏輯;而構造者、協調者與控制者皆屬于設計對象,引入的目的隻是為改進設計品質,本身與領域邏輯無關;但它們在軟體設計中的地位卻舉足輕重,沒有它們,設計就可能走向混亂,無法保證重用性與擴充性,并導緻系統對象之間的協作變得複雜。

如果我們能識辨出系統模型中各種對象的角色,就可以根據角色的特征來配置設定角色。又或者,我們可以根據角色來判别現有的職責配置設定是否合理,是否均衡,甚至能夠幫助我們找到缺失的對象。每當我們在配置設定職責時,若有顧此失彼的感覺存在,就可能說明缺乏了承擔不同角色作用的這一類對象。找到它,并給它以承擔職責的權利,設計一定會大為改觀。

找對象不是一件容易的事情,要找到一個好對象更其不容易。這句話适合于單身汪或者單身喵,同樣适合于程式員和設計者。沒有定論的方法與過程可以幫我們解決這個問題,正如你無法單純地拿相親标準去尋覓那個未來陪伴你一生的另一半。學會分辨對象的角色,或許是我們可以嘗試的。它在一定程度上分解了設計難題,制造了限制,反而令你在相對狹小的空間内更顯遊刃有餘。