文章目錄
- 2.1.1 固有資訊
- 2.1.2 動態資訊
- 2.1.3 屬性封裝
- 2.2.1 貧血VS充血
- 2.2.2 職責單一
- 2.2.3 誰擁有資料,誰持有行為
- 2.2.4 方法屬於客戶
- 2.2.5 行為封裝
關於業務對象本質的思考
[摘要]:本文基於前人在OO、DDD等領域的研究成果,結合個人工作經驗及感悟,對業務對象(Business Object)的本質進行了提煉和總結,並就BO三要素以及BO的擷取和驗證等問題進行了闡述,旨在加深OOA/D人員對BO的正確認識,分析並設計出更優質的軟體產品。
1 引言
對於採用OO思想,並具有N層架構的電腦程式而言,業務對象(Business Object)一般位於商務邏輯層(也叫領域層[1]),作為領域模型元素的一部分,描述了來自於業務域中的一個人、事、物或概念[2],主要用來解決商業邏輯(即業務操作)問題,是為實現當前軟體(或系統)的某一(或某些)特定功能而服務的。因此,它僅僅是從當前業務域的角度,對現實世界的一次抽象。
業務對象、業務實體、實體、領域對象在某種程度上是可以互換的術語[此處僅僅說是“某種程度上”,目的在於分析BO三要素的實質,針對於不同的理論,這幾個名詞確實存在差別,但本文不做贅述]。
2 對象三要素
依據Wikipedia對object的定義[2],對象具有以下三個要素:
■ 標識(identity),是唯一區別其他對象的標誌;
■ 狀態(state),描述對象所蘊含的資訊;
■ 行為(behavior),對象所持有的、描述對象如何被使用的方法。
BO are state and behavior together.下文僅對狀態和行為進行闡述,BO的標識不做贅述。
2.1 狀態
相對於欄位、變數、屬性等詞,用“狀態”一詞來描述對象的“性格特點”最為合適。欄位、變數、屬性等,無非是描述對象狀態的不同表現手法。就目前個人對BO狀態的理解而言,BO的狀態資訊有兩類:固有資訊和動態資訊。其中,固有資訊是對象從誕生時與生俱來的資訊(就像一個新生命的誕生一樣,出生年月日、膚色、性別等資訊是生來就有的);動態資訊,即隨著BO的成長,它會走入某一個人生情境中,扮演了某一個角色,從而在這個情境中被賦予了一些額外的資訊,如:小明是個學生(從入學時,學生的基本資料被賦予),某天他去市圖書館借閱圖書,此時他在借書這個情境中扮演了借書者這個角色,從而具有了借閱證、借閱資訊等動態資訊。
2.1.1 固有資訊
個人認為業務對象在被構建(初始化)後,應達到一個相對比較穩定,且具有一定業務含義的狀態,即業務對象的屬性應已進行了相應的初始化設定,這樣的構建才算合理、完整。與人類世界類似,人在出生時,就已經構造好了嬰兒的鼻子、眼睛、手等內容,雖然此時他還沒有衣服、母語、身份證等資訊,但他在出生時,是一個相對較穩定且有意義的個體,就可以完成嬰兒的核心操作,如:哭、呼吸、揮手等。試想,一個對象在構建之後,還需要通過頻繁的setter()方法後才可以完成核心業務操作,這是良好封裝的表現嗎?對調用者而言,核心目標是使用BO所公布的方法,用之前還要先setter這個,再setter那個,顯然是一種很煩人的,至少setter()這個和那個,不是調用者的本意,更不是調用者的職責
提前構造好該構造好的資訊(注意:不是所有的資訊都要一股腦的全構造好)是保證BO不被濫用的第一步。此處容易產生疑問,若一個BO在構造時涉及大量的初始資訊,也統統的從建構函式裡面作為參數注入嗎?關於這方面的顧慮,我的解答:首先,構造其本質就是初始化BO執行個體,無關痛癢的new 一個空的或不完整的BO執行個體一樣是沒啥意義(甚至是擾亂視聽);其次,參數多,說明對象將被構造得徹底,帶來更多穩定性,可試圖對參數進行分類封裝,以減少這方面的糾結(但個人認為,沒必要);最後,建議結合spring的依賴注入、Bean的配置等內容進行理解。
2.1.2 動態資訊
進入某一情境,扮演某一角色(我更喜歡用“戴上帽子”這個說法),將擁有額外的動態屬性。同“固有屬性”的構建,某一業務對象走入特定的情境,扮演了另一角色時,也應該將這些動態屬性予以設定。一句話,對象應盡量在構建自身的過程中完成自身狀態的設定。
Student trace = new Student(“trace”,1900.10.10,man….);
trace.drink();
IBorrower borrower = trace.ActAs<IBorrower>();
borrower.borrowBooks();
2.1.3 屬性封裝
在OO的世界裡,封裝的概念是最簡單的,但卻是最關鍵且最難以把握的。對內部資訊的封裝是作為合格OOA/D人員必須遵循的最基本原則。
【問】:不封裝屬性,將業務對象的資訊暴露出來,程式正常實現了,好像也沒出什麼大問題?!
【答】:恭喜你,你通過對那麼多唾手可得的資訊,實現了業務功能(暫且不說程式碼結構如何,是否內聚/低耦),看上去沒有問題。但若需求變化,甚至是高層策略都變化的話,如何應付?
資訊的不封裝(或封裝不完整),已經讓太多的程式員吃盡苦頭,而且往往都是自己親手埋下的苦果,並總伴隨著“如果重新再來,我肯定不會這麼設計”的後悔和無奈。資訊不封裝(或封裝不完整)帶來的副作用:
1、內聚,難。業務對象的資訊過多的暴露出去,容易滋生強盜邏輯,想捏回去形成一團,難!資訊全部都暴露給外界,調用者還需要你BO幹嘛?原因很簡單:我能夠伸手拿到你的任何資訊,想實現什麼就實現什麼,可以為所欲為。如果你還有膽量暴露一些更改BO屬性的許可權,那我豈不是想怎麼改就怎麼改。你(業務對象)能控製得了?你還想內聚?做夢!
2、解耦,難。一個成語叫“覆水難收”,放出去的資訊將被調用者肆意使用,而且呈現出快速蔓延的趨勢,一張複雜的耦合網必然產生。等回頭開始重構解耦時,發現堆積如山的代碼、耦合似網的結構,已經讓你無從下手。動一下,就引起全身陣痛。常見的做法:(1)刨一小塊,改改變數名、方法名,移一些代碼,用介面再封裝一下(隔離嘛),迴圈的修改,最終發現進展依然是非常緩慢,幾乎還沒觸及到業務核心;(2)先用方法(1)試試,一陣子後,MD,煩死了,直接推倒重新搞。這些做法還需要考慮一個問題:放出去的介面和資訊已被調用者大量使用,怎麼辦?
針對上述的苦痛,推薦下面的做法:
1、在對象構建時,把能設定的狀態資訊儘可能的予以賦值,提前封裝;
2、儘可能的不要公布內部資訊。能夠private的,儘可能的私人。除非迫不得已,盡量不要將setter() 放出,僅使其read only。[個人感受]:以前寫代碼,從來不顧及private/protect/public,統統public,導致的惡果已經讓我吃了好幾壺。
2.2 行為
業務行為才是軟體真正所關注的問題,對象的行為方式是對象價值的重要體現,也是區別於其他對象的重要標誌。因此,我們說“BO因職責而存在!”。
2.2.1 貧血VS充血
關於貧血模型和充血模型的爭論從未休止過,但本文僅從BO的角度論述二者的差異(僅為個人之見)。
■ 貧血對象
由不具有任何行為的業務對象形成的領域模型,稱為“貧血模型”[2]。對只有屬性的getter/setter方法,不具有業務行為的BO,可認為是“貧血對象”。喪失商務邏輯行為的貧血對象,和Value Oject類似,扮演了Data Container的角色,而在業務域中的邏輯操作方面將失去能力(或被遺棄、邊緣化)。
■ 充血對象——按大師的說法,與BO直接相關的行為職責將劃歸到BO中,使其在領域模型中扮演重要的角色。
但是二者不能絕對的說誰好、誰不好,應該一分為二的去看,它們各自具有其特點,應用在不同的情境中。特別地,對於那些需求難以完全吃透、明確,或許用貧血模型較充血模型要更好把控局勢。“使用者需求——業務——領域”是一個對知識掌握程度遞增的過程,領域模型的建立應基於對客觀業務域的透徹掌握,不能偏左,也不可以偏右(不就是博弈嗎?沒有最優秀的東西,只存在考慮諸多因素下的較為合適的東西)。
2.2.2 職責單一
在談“職責單一”前,先說說business core。BizCore是系統核心價值(業務骨架、靈魂)的體現。本人對BizCore的理解:它應該是最精鍊、純粹、簡單、直接、輕量級的業務核心。因此,不屬於核心商務邏輯範疇的職責和行為(如:持久化操作),盡量拋出去交給該處理它們的對象去處理。[在寫下這段文字時,很想再加入DDD的某些概念,但是用一個新概念解釋一個原本不太晦澀的概念,實在是不妥]
有了這個邊界(或者說原則),再談談SRP(職責單一原則)。經常會看到God class(上帝類,你可以認為它就是一個充血充得快爆掉的對象),它幾乎可以幹所有涉及到它的工作,從而形成代碼有幾百行乃至上千行的牛X類(本人見過的最牛X類,接近5k行代碼)。請問:有了這麼一個牛X類,其他類不就成了浮雲和雞肋了嗎?這樣可能引發很多問題:
1、理解難。幾千行的代碼,沒有幾個人有耐心閱讀,幾乎沒人能夠完全理解其表達的業務含義。我敢保證:這樣的類,會對方法名的表意性帶來巨大的衝擊,如:getFlowDataFromTaliformAfterSave….(),哇靠!這個方法名算好的,更匪夷所思的方法名將讓執著的程式員頭都要爆掉。
2、修改難。沒有很好的理解,如何修改?如何重構?
3、擴充難。丟了它玩不轉,不丟它又引來一堆麻煩,對於一個幾千行的實作類別來說(除非大多都是public static 的方法),吃資源不說,介面隔離、應對擴充方面也是比較吃力的。
2.2.3 誰擁有資料,誰持有行為
BO行為的本質是對BO自身狀態的改變,以實現營運目標,並且這種狀態的更改可能還需要其他共同作業者的參與(關聯關係)。因此,我們可以說“誰擁有資料,誰就持有更改這些資料(狀態)的行為”。
現實中,跟BO有關係的行為可能較多,全部納入到BO中,會造成BO的臃腫和汙染,違背SRP。所以,行為的歸屬需要遵循一些原則:
■ 可重用度高或是對象固有、與BO狀態密切關聯的方法放在BO中。
■ 可重用度低或者不是對象所固有(而依賴於特定情境)、與BO狀態沒有密切聯絡的方法放在BO管理者或服務層。
上面的原則,和DDD中關於領域模型(domain model)的行為歸屬類同。甚至我覺得領域模型中的對象,即為最純粹的BO。
2.2.4 方法屬於客戶
BO就是一個黑盒子,其中包含了邏輯和資料,而對象的使用者不知道裡面有什麼資料,也不知道實際的運行邏輯。使用者所能做的就是與對象進行互動,以完成當前的營運目標。因此,對象的行為是為客戶而定。
正如前面我們所說,行為(方法、職責)是BO存在的根本,而行為就是為了互動,為供調用者所使用。調用者會根據自己的需要,向它認為應該由誰提供行為的BO發出操作申請,一切都是以“客戶(調用者)”為中心而服務的。
2.2.5 行為封裝
關於對象行為的封裝,有兩個層面:
1、千萬不要以為自己擁有這麼多資料,就可以肆意的發布任何方法,真正被調用者使用的方法其實也很少。跟現實中的人一樣,過多的對外暴露行為,別人會把你的資訊四處傳播,到時候你想改變一下自己的形象,難!因此,僅對外暴露穩定的、合理的、剛剛滿足客戶需求的API就足夠。
2、行為在不同層級的範圍內應該封裝,僅自己內部使用的話,private就OK了,如果需要讓子類持有,protect一下就OK了。總之,在定義BO的行為時,握緊手中的那把尺子,盡量謹慎行事,不要過早的給自己挖坑、負債。
————————以下內容在下一篇文章中涉及————————
3 對象的擷取
1、原始資料中的名詞擷取;
2、抽象角度
3、抽象層次
4、業務域vs領域vs技術域
4 良好BO的驗證5 總結與展望
1、基於資料庫表的分析與設計;
2、封裝變化
3、OO需要一分為二
4、關於“業務架構師”
6 參考文獻
[1] Eric Evans. 領域驅動設計[M] 清華大學出版社
[2]大象:Thinking in UML
[3] http://en.wikipedia.org/wiki/Business_object
[4] http://stackoverflow.com
[5] Martin C Robert. 敏捷式軟體開發 (Agile Software Development) 原則、模式與實踐(C#版)[M] 人民郵電出版社 p107
[6]Steve Freeman, Nat Pryce. Growing Object-Oriented Software, Guided by Tests.