(團隊內部技術分享摘要)
目錄
- 目前開發實踐中的問題
- 相關設計模式和架構概述
- 其他設計/架構模式
- 相關概念分析
目前開發實踐中的問題
- 業務邏輯泄露,本應屬于 Service 的業務邏輯泄露到其他各層中(Controller、Repository、View等),而原本內容豐富的 Service 反而變成了貧血類,
- 全能Service,主要表現是超多的代碼(如vshop的商品和訂單的Service代碼都在1000行以上)和多方面的功能,例如OrderService,幾乎只要是跟訂單相關的都在此Service中,而沒有進行進一步的精細建模,
- 重復功能,表現在(特別是在跨模塊時)重復的Service(如MemberService)和重復的方法,這些重復Service大部分地方一樣,少數地方有區別,
- 一些Service中充斥著各種各樣的查詢功能(串列和單記錄查詢),讓這個Service看起來很怪異,
- 貧血模型,基本都出現在Service層,多出現在查詢的地方(串列、單記錄),因為我們默認約定Controller必須通過Service進行讀寫,而不能直接訪問Repository,而實際上很多查詢操作只需要Repository直接回傳的資料即可,Service不需要做任何操作,
- 以技術視角劃分模塊和系統,例如,所有的作業任務都放在一個系統(如message-center),而作業必然會出現各自業務邏輯(如會員合并),因而同一套業務邏輯必然會出現在多處,
- “前后臺分離”,此處的分離是指按照前后臺站點分離出完全獨立的兩套系統(如一個會員系統,分成了給商戶用的系統member-center和給C端用戶用的系統v-member,導致很多業務邏輯需要在這兩個系統中重復實作),
- 經典的“framework問題”,這個問題幾乎困擾著全公司人,而且還會繼續困擾下去,framework問題的實質是敏捷團隊/公司和傳統架構思維的阻抗,它實際上正印證著康威定律:組織結構決定著軟體架構,該主題將在后面詳細討論,
- OO(面向物件)無用論,一般在應聘時,我們都會在簡歷上寫上“良好的OO基礎”,但實際上我們骨子里是對OO持有懷疑和抵觸的,而且實際開發中也是自覺不自覺地在面向物件框架中進行著面向程序的開發,問題是,為何我們那么抵觸OO,那么喜歡程序式開發?另一個問題是,為何我們需要擁抱OO,相比于程序式開發能帶來哪些好處?
- 二維設計,或者說:“當我拿著MVC這把羊角錘時,全世界都是釘子”,當我們用MVC這一單一設計模式去解決一切問題時,就陷入了二維設計,我們無法立體地看待問題,無論是思考(設計)還是編碼都是在平面上進行著(如Controller -> Service -> Repository的形式化呼叫),這種思維方式往往導致形式化的流程和約束,而形式化的東西又往往束縛了人們的思想,進而不再去思考,
相關設計模式和架構概述
-
控制反轉(IoC) :或說依賴倒置,屬于SOLID五大原則之一(D),
描述:高層模塊不應該依賴于低層模塊,二者都應該依賴于抽象,抽象不應當依賴于細節,細節應當依賴于抽象,
注意這里提到高層和低層,說明該原則主要是用來解決跨層的依賴問題(例如我們的Service和Repository),一般,高層需要用到低層的東西(Service使用Repository),對低層產生依賴,那么當需要替換低層實作時,就需要改動高層代碼,IoC要求,高層不應當依賴于低層實作,低層實作的變更也不應該影響高層,那么如何做到呢?介面,或說抽象,高層和低層遵守相同的抽象,并唯一據此抽象(介面)通信,
再從另一個角度理解這個問題,就是將物件的使用和創建分離,使用者(呼叫者,依賴方)只是使用物件,而不負責創建它,創建作業由外部負責,這樣當需要更換被依賴方的實作時,無需修改依賴方代碼,比如說,我們的Service需要使用Repository,傳統做法是在Service里面new一個Repository,現在要將原Repository替換成其他的(如原來是Db倉儲,需換成Redis或nosql倉儲),則需要修改所有使用了該Repository的地方,當使用IoC時,由于該Repository是由外部創建的,只需要調整外部.
這種由原來的內部new變成由外部注入的實作方式,稱為依賴注入(DI),Yii框架里面的建構式注入和服務容器(\Yii::$app->container)是DI的兩種實作方式,
IoC是設計原則,DI是該原則的實作方式,
雖然該原則主要用來解決跨層依賴問題,但他同樣適用于同層之間的解耦,如果這些依賴之間不是高內聚的話,(但是,每當出現同層的低內聚類之間的依賴時(例如聚合之間的呼叫),首先需要考察是否需要一個更高層次的協調者,例如一個Service),
另外需要注意的是,不要濫用IoC,雖然IoC是用來實作松耦合的很好方式,但軟體設計中除了“松耦合”原則還有“高內聚”原則,高內聚的類之間是可以強耦合的,否則,很容易出現過度設計的問題,
參見:http://www.tuicool.com/articles/JBRBzqm 《從百草園到三味書屋》,laravel作者著,里面有很好的示例詮釋了依賴反轉,
-
六邊形架構(埠-配接器模式) :
作業中,我們經常遇到以下難題:
- 業務邏輯泄露到各層中,以及業務代碼對輸入輸出的強依賴(請求物件、資料庫存盤物件等),很難干凈的進行單元測驗;
- 開發時依賴于資料庫、快取系統的正常運行,一旦這些掛了,就無法開發了;
- 當需要切換一個底層的技術實作時,需要改動相關業務層代碼,
六邊形架構的目的:讓程式能夠以一致的方式被用戶、程式、自動化測驗、批處理腳本所驅動;并且,可以在與實際運行的設備和資料庫相隔離的情況下開發和測驗,
例如,我們有會員合并的需求,而該需求的出現場景很多:web應用、api、后臺批處理、訊息佇列異步處理等,目前,我們是在各場景分別寫了一套合并邏輯,很難維護,而且合并邏輯本身和存盤層是強耦合的,我們無法在忽略存盤層的情況下進行單元測驗(實際上,目前的代碼根本無法進行單元測驗),按照六邊形架構,web應用、api、后臺處理都屬于不同的輸入源,這些需要和應用程式本身(會員合并業務)解耦,這些不同的輸入源的不同輸入資料需要通過各自的配接器適配成應用程式能理解的資料格式,配接器和應用程式介面遵守共同的契約(inteface),唯一通過該契約通信,輸出端也是一樣,應用程式本身不依賴于特定的輸出端實作(web、api、單測、資料庫),實作中也是通過各自的配接器根據各自的輸出端技術將應用程式的輸出轉換適配成具體輸出端需要的資料,如html、xml、具體資料庫所需要的,
當我們將應用程式中對外部的依賴從具體實作改成對介面的依賴,并且將泄露到外圍(控制器、倉儲層等)的業務邏輯封裝進應用程式內部后,就可以很容易進行單測,例如很容易用mocker代替實際的web輸入、存盤層、快取組件等,
下面是六邊形架構的圖解:
這里有兩個層:業務邏輯層 (領域 + 應用程式,目前可簡單理解為領域層或業務邏輯層),也稱之為應用程式(內層、內圓); 外圍設施層 (web、單測、資料庫、快取服務器等,外層、外圓),內層不依賴于外層的存在而存在(如業務邏輯層在資料庫不可用時應當仍然可通過其他方式代替資料庫來運行),內層通過暴露埠(api或函式呼叫)為外層提供功能(服務)或從外層接收資料——這很像作業系統的埠,埠的表現形式是契約interface(api或函式的入參以及回傳值),外層并不直接和內層打交道,而是通過各自的配接器來實作通信(控制器就是典型的配接器),配接器將內層輸出轉換適配成其為之服務的外層設備所需的資料,或將外層設備的輸入資料適配成內層所需要的資料,配接器和內層唯一通過契約(interface)通信,
舉個例子:電腦需要接收各種外設的輸入進行處理,這里的電腦就是內層應用程式,各種外設(u盤、手機、網路等)屬于外層,為了對外通信,主機上有各種插槽(埠),每個插槽遵循不同的規格,外設通過配接器(如各種資料線和資料轉換設備)和電腦進行資料交換,而配接器需要遵循兩端協議:一端是電腦插口、一端是具體的外設插口,
IoC是實作六邊形架構的有效手段:內層不應當依賴于外層實作,雙方都應當依賴于介面定義——這也正是IoC的描述,但六邊形架構還有一點:內層的業務邏輯不應當泄露到外層,因為一旦業務邏輯泄漏到外層,那么內層就不再是通用的、與外層實作無關的了,也就無法進行多邊適配了,
另外需要注意的是,這里用的是“內層”和“外層”,并沒有用上層和下層的說法,這里強調的是多邊適配,而不是類似網路模型中的七層架構,
我們發現,在實際應用中,我們或多或少用到了六邊形架構的東西(如IoC),但為啥代碼還是難以測驗難以維護呢?應用了六邊形的某些東西不代表整個應用遵循六邊形架構,比如我們用DI,但并不是特別清楚為啥要用DI,以及哪些地方要用哪些地方不需要用,更重要的,我們的業務邏輯并沒有進行很好的封裝與解耦,自然難以維護,
參考文章:http://blog.csdn.net/zhongjinggz/article/details/43889821
-
GRASP 九大設計模式:
GRASP系列設計模式主要是用來解決OOD中模塊劃分、職責分配問題,此處我們重點看下資訊專家模式和創建者模式,
-
資訊專家模式:將職責分配給擁有履行一個職責所必需資訊的類,即資訊專家,
資訊專家模式解決的是由誰來承擔該職責問題,首先考察該職責(方法)需要用到的資料(資訊)從何而來,一般是將職責分配給主資料源類,
例如,論壇系統有文章Article類和作者Author類,有發布文章的職責(publish方法),那么該職責由哪個類負責呢?先假設由Author類負責(從需求描述“張三發布一篇文章”來看,貌似屬于Author的職責),像這樣呼叫:Author::publish(Article),我們發現,publish內部使用到的資料基本都是從Article來的(除了作者資訊),當Article的資料結構有所變化時,同時需要修改Author類,并且在publish內部還需要呼叫Article類的方法進行業務規則校驗,兩者之間產生了很強的依賴關系,同時違反了SOLID原則中的單一職責、開放關閉等原則,如果將publish方法放到Article類中,那么所需要的資料都是自足的,校驗規則也是內在的,不需要對外公開,也就保證了修改規則時不影響其他類,
資訊專家模式需要結合高內聚和低耦合模式一起使用,比如物體物件的持久化問題(save()),持久化所用到的資料顯然是物體物件的,按照資訊專家模式,save()方法應該在物體類中,但從職責上來說,持久化屬于低層技術實作,不屬于業務邏輯,不應該由物體承擔——我們用單獨的倉儲來負責物體的持久化作業,
-
創建者模式 :誰應該負責產生類的實體?
該模式解決的是類的創建職責問題,B包含或聚合A,或直接使用A,則由B來創建A,
比如文章Article和作者Author,Article實體擁有Author實體的參考,那么由誰來創建這個Author物件呢?一種可能是通過Article建構式從外界傳入,由外界創建Author物件,但這樣就將Article的內部細節暴露給了外界,更好的做法是由Article內部自己創建Author物件,隱藏實作細節,
創建者模式的使用同樣需要結合高內聚低耦合模式,
再看另一個例子:每當文章發布后,需要給相關訂閱者發送通知,ArticleService呼叫Article::publish()后,需獲取訂閱者串列并呼叫Email::sendMessage()給他們發送郵件,這里ArticleService使用了Email實體,按照創建者模式,是否應該由ArticleService內部創建Email實體呢?假如是的話,考慮下當以后需要替換Email實作時會發生什么?此時就需要挨個去找哪里使用了該Email實體,然后一個一個替換,顯然此種情況需要用控制反轉原則,由外界注入Email實體,
這兩個例子有何區別?
前一個例子中,Article和Author屬于聚合關系,是較強的關系,他們共同組成了業務整體,因而可以采用創建者模式,而且也應當使用該模式以隱藏內部細節,后一個例子中,ArticleService和Email純粹是使用關系,是很弱的關系,而且兩者是跨邊界呼叫(ArticleService屬于領域層,Email屬于基礎設施層),在六邊形架構中,ArticleService在內圓,而Email在外圓,內圓不應當依賴于外圓的實作,因而這里不能采用創建者模式,而應當采用IoC以保持低耦合,
在創建者模式的條件串列中,“使用”列在最后,是最弱的關系,實際使用中,如果兩者僅僅是“使用”關系,則要慎用創建者模式,
-
-
SOLID 原則:
SOLID原則是面向物件設計和編程中最基本也最重要的五大經典原則(該單詞是該五原則的首字母縮寫):
-
單一職責原則 :有且只有一個(一類)原因(理由)去改變一個類,
文章Article有publish()用來發表文章,也有save()用來保存文章到資料庫中,
現在來考察下save():將文章物件持久化到資料庫,某一天,當持久化策略變了(用mongodb代替mysql),我們需要替換持久化引擎,此時就需要去修改這個save()方法了,“改變持久化策略”顯然和Article沒有直接關系,但卻影響到了Article類,這就違反了單一職責原則,
-
開放封閉原則 :代碼對擴展開放,對修改封閉,
文章發布后,需要給訂閱者發訊息,前面的做法是在ArticleService::publish()方法中獲取訂閱者串列,并呼叫Email::sendMessage()給其發訊息,
現在,有這樣的需求:只給最近三個月看過該作者文章的訂閱者發訊息,此時我們需要修改OrderService,在發送之前對每個訂閱者做檢查,再過幾天,又有需求:只給關注了相關欄目的訂閱者發訊息......你會發現,隨著需求的每次改動,ArticleService會被沒完沒了地改來改去(實際中我們正在做這樣的事),這里對修改是開放的,
因給訂閱者發訊息規則的變動而需要修改ArticleService,這本身違反了單一職責原則(SOLID原則都是互通的,違反其中一個往往也違反其他的),可以在ArticleService::publish()中發布一個article-published事件,外部訂閱該事件,這樣可以在事件訂閱端做任何業務擴展(對擴展開放)而不影響這里的類(對修改封閉),
-
里氏替換原則 :一個抽象的任意一個實作,可以用在任何需要該抽象的地方,
有個IAnimal抽象定義了run()和sound()方法(發聲),下面有實作類:Bird、Earthworm(蚯蚓)、Person,Bird和Person都對sound()做了各自的實作(發鳥聲和人聲),但Earthworm::sound()卻throw了個例外,現在有個AnimalTrainer::train(IAnimal)這樣的呼叫,想想會發生什么?當我們傳入Earthworm物件時,其運行結果是未知的,有可能拋例外(如果AnimalTrainer呼叫了sound的話),我們通過train(IAnimal)的宣告無法知道它如何使用這個IAnimal,而根據里氏替換原則,Earthworm自然應該被AnimalTrainer正確使用(而不是拋例外),因而這里Earthworm的實作違背了里氏替換原則,如果實作類既要實作一個抽象,又不想去實作該抽象的某些契約(通過拋例外來抗議),說明你的抽象設計有問題,
-
介面隔離原則 :在實作介面時,不能強迫去實作沒有用處的方法,
還是上面的例子,訓練師去訓練蚯蚓發聲是枉然的,Earthworm::sound()是完全沒有用處的,要么放著空函式什么都不做,要么拋例外,這里的設計就違背了介面隔離原則,
-
依賴反轉原則 :
該原則在前面已經單獨討論過(因為對六邊形架構太重要了),此處不再贅述,
參見:http://www.tuicool.com/articles/JBRBzqm 《從百草園到三味書屋》,laravel作者著,里面對SOLID原則有很好的示例講解,
下面舉個綜合例子:
客戶購買商品,下單時,系統需要進行各項校驗,
假設在OrderProccessor::confirm()中進行訂單校驗(該類維持一個對Order實體的參考),
最開始只需要校驗相關商品是否有足夠庫存,我們創建Order::validate()執行這些校驗,由于validate所使用的資料大部分都可從Order物件得到,這符合資訊專家模式,Ok,
某天,接到一個添加校驗規則的需求:校驗下單者是否符合下單規則(只有業主才能下單),我們需要改Order::validate()方法,然后我們發現訂單校驗規則的變化會導致Order的修改,這違反了單一職責原則和開放封閉原則,于是我們決定將“訂單校驗”業務邏輯抽離出來形成OrderValidator類,由OrderValidator::validate(Order)實作校驗,這樣就隔離了校驗規則改動對訂單類的影響,
但是我們后來發現,一個validate()方法搞定所有校驗,導致validate()這個方法過于臃腫,于是我們對校驗規則分類后抽離出validateGoodsStock()、validateBuyer()等獨立的方法,然后在validate()中呼叫這些方法,
過幾天,又要加個校驗規則:訂單價格是否合法,于是我們又加個validatePrice()方法,雖然說OrderValidator隔離了校驗對Order類的影響,但每加個規則就去改下該類,這違反了開放封閉原則,
有沒有什么辦法能夠隔離校驗業務的變動對OrderValidator的影響呢?
答案是抽象,
我們抽象出IOrderValidator介面,定義一個validate(Order)契約,然后創建GoodsStockValidator、BuyerValidator、PriceValidator等實作類實作該介面,在其validate(Order)中實作上述種種校驗,然后給OrderProccesor注入一個包含IOrderValidator的集合,在confirm()中順序呼叫每個驗證器的validate方法,
現在,需要增加校驗規則,沒問題,創建一個新的IOrderValidator實作類并放到校驗集合中即可——該設計對擴展開放(通過創建新的校驗器類),對修改封閉(不需要修改其他的類),
上面的幾個設計模式都是非常基礎非常通用的,是實施OO必須掌握的,它們共同的基礎原則都是“高內聚低耦合”,進行OOD時必須時刻進行這些原則反思,
-
其他設計/架構模式
-
DDD(領域驅動設計) :
領域驅動設計指出傳統的需求分析和模型設計、代碼撰寫是相互割裂的,傳統有需求分析師和系統設計師兩個獨立的職位,這種割裂導致相互之間的不匹配,比如系統設計不能完全反映出需求分析,而代碼又和系統設計割裂,各自有一套自己的私有語言,相互之間很難溝通,
DDD強調需求分析、建模和編碼的內在統一性,三者(以及執行三者的人)之間使用一致的領域通用語言溝通,因而業務專家、設計師、程式員之間能夠很容易達成共識,
現實情況是,程式員和業務專家(以產品經理為代表)之間的溝通要么存在嚴重的鴻溝,要么使用非業務(往往是技術性的)語言溝通,背離真正的業務領域概念,程式員很喜歡用技術性語言(甚至直接拿資料庫說事)和別人(哪怕是非技術人員如客服、銷售)溝通,導致各執一詞,一般敏捷團隊往往只有一個產品經理,而有好幾個技術人員,往往會出現以技術性語言主導溝通的場面(如果產品經理本身不注重對團隊的業務語言引導的話),
程式員為何那么喜歡用技術性語言和別人溝通?一方面,程式員的溝通物件常常也是程式員,技術性語言溝通成本最低;另一方面,他們往往在溝通的同時就在想著實作方案(或者說溝通本身就是對實作方案的描述),
然而,技術語言溝通對業務模型的建立有著很嚴重的損害,技術本身和業務是兩個領域的東西,技術語言在現實中最典型的代表就是“資料庫語言”,比如“某個時候將某表的某欄位標記為1”,這于業務本身無任何意義,這種思維導向會讓我們腦海中越過建模而直達存盤實作低層,另一方面,這種技術與業務語言的混雜會讓業務邏輯本身耦合進存盤層的設計中,例如,單從存盤設計(技術實作)上來說,“登錄狀態”應當由單獨的欄位來標記,而在業務領域中,“登錄”與“退出登錄”操作會導致另外的狀態變化(存盤設計上表現為另一個欄位),當我們在進行存盤層設計時過多的代入業務邏輯本身(或者毋寧說在業務邏輯描述時過多地代入存盤層的技術實作),我們可能會用另一個欄位(存盤著其他的狀態)來代表登錄狀態(并且很自豪地認為這樣能節約存盤空間——典型的技術主導一切的思維),這里的問題是:登錄狀態的存盤實作依賴于業務邏輯,由于業務邏輯是不穩定的(相對于存盤層),因而這里作為最底層的存盤層設計也是不穩定的,(實際上會員的登錄狀態存盤就存在這樣的問題)
DDD強調:
-
從需求分析到代碼實作到測驗的整個程序各人員之間的溝通需要使用一致的無歧義的領域內通用語言,
-
該通用語言必須能夠準確反映業務需求和領域知識(而不是反映技術實作),
對于程式員來說,DDD的這種思想可概括為:代碼即模型,編碼即設計,我們寫出來的代碼,類與類之間的呼叫關系,方法、變數的命名都要反映領域通用語言本身,DDD非常強調命名,對于DDD來說,編程本身就是語言活動(不是機器語言),DDD強調語言的重要性,這語言是人類的語言,而且是某特定領域下的人類語言,
現實情況是,我們的代碼中充斥著大量的面向資料庫的語言,例如update()、delete()充斥在各種業務代碼中,“將欄位A的值更新為b”是技術(資料庫)語言,不是業務領域語言,更有甚,在控制器層寫個UpdateFields()搞定一切更新操作,
然而,并不是所有的技術實作都要以當前業務領域語言來詮釋,實際上這也是做不到的,有些技術實作并非屬于業務領域之內,例如持久化存盤、事件系統、訊息佇列等,這些不屬于當前業務領域的技術實作當然也就不需要遵守該業務領域通用語言(它們需要遵守的是各自的領域語言規約),這種業務領域邊界在DDD中叫“限界背景關系”,背景關系內的東西屬于六邊形架構中的內圓部分,而外部的東西屬于外圓,內外圓通過契約適配通信(配接器),
DDD并非泛泛的理論闡述,它有一套詳細的實作體系(方法論),如物體、值物件、聚合、聚合根、領域服務、倉儲、各種設計模式等,此處不做詳細闡述(那得寫成一本厚厚的書,而且DDD本身是很注重實踐的,一百個人就有一百種實作方式,重在掌握其核心思想),
這里提出DDD,重點在于對比我們現在使用的開發/設計模式:面向服務設計以及面向資料庫設計,在DDD中,物體是核心,服務只是輔助,而資料庫則是領域外的基礎設施,
-
-
CQRS(命令與查詢職責分離) :
命令:會導致物體狀態變化的操作(反映在資料庫上的更新、洗掉、插入等);
查詢:不會導致物體狀態變化的操作,
CQRS原則:命令中不要有查詢,查詢中不要有命令,例如常做的在修改方法中同時查詢并回傳某記錄,這就是違反CQRS的,
CQRS的目的是為了應對這樣一個事實:命令模型與查詢模型往往存在很大的不同,想想我們的資料庫設計就很好理解,我們進行資料庫設計時,一般是按照命令模型進行設計的,這和我們腦海中的業務模型比較匹配,而報表則是典型的查詢模型(分析模型),一般情況下,按照命令模型設計的資料表結構是滿足不了查詢模型的報表分析的,因而,為了出報表,要么需要寫很復雜的sql,要么進行資料加工清洗以得出符合條件的查詢模型,顯然,在命令模型上執行分析查詢性能是非常低下的,
可以在不同層面上使用CQRS:
-
傳統意義的讀寫分離,命令和查詢使用不同的庫,但兩個庫中的表結構相同,
-
代碼層面分離,存盤層不分離,(有可能采用讀寫分離,但表結構是一樣的)
-
代碼和存盤層都分離,這也是嚴格意義上的CQRS,這里寫表是基于命令模型設計的,讀表是基于查 詢模型設計的,讀和寫是通過事件來同步的(命令端執行完畢后發布相應事件,查詢端訂閱事件并更新 查詢模型),在代碼和存盤層進行命令與查詢分離,在兩端各自采用最適合的實作方式,以達到最優設 計和最好的性能,
嚴格意義上的CQRS實作起來很復雜,要求基礎支撐夠健壯才行,
我們這里提出CQRS,一方面是為了指出以上事實,另一方面,在實踐中我們可以嘗試第二層面的CQRS,以獲得代碼層面帶來的益處,如快取管理、兩端可采用不同的設計模式(如命令端采用DDD,查詢端采用傳統的MVC),
-
相關概念分析
-
關于Service(Service的含義以及面向服務編程有何問題) :
當我們說“服務”時,我們強調的是其為外在它方提供功能,服務本身具有動詞性,核心是其功能性,該功能當然由服務提供者提供,但我們并不關心服務提供者本身,服務具有外向性,即服務存在的價值取決于其對他方(而非自身)的價值,因而,當我們以服務為核心時,我們就必然會以外在旁觀者的角度審視其外在價值,當我們說“訊息佇列服務”,我們看重的是訊息佇列給我們業務系統帶來異步解耦的好處,而不是訊息佇列本身的內部機制,
這種看待問題的角度對建模是無益的,當提及服務時,總是有個“我”存在(就是那個旁觀者),而建模強調的是達到“無我”的境界,需要消除這個旁觀者,化身為模型本身,從事物內在角度去思考問題,模型是內在自滿足的,它本身具有特定的行為,而不是靠各種服務“提供”給它,
舉個現實的例子,軟體外包公司對外提供軟體外包服務,當我們作為甲方(也就是旁觀者)接受其提供的服務時,只需要告訴外包公司我們有什么樣的需求,雙方達成共識,簽訂合同,最終我們從外包公司那里接收符合合同預期的軟體成品,我們并不關心外包公司內部具體怎樣運作,哪個團隊負責ui設計,哪個負責編程等,但是,對于外包公司本身來說,這種視角是行不通的,它必須設計一套詳細的、健全的公司運作體系以保證其可以提供該服務,也就是說必須對“外包公司”這個概念進行內在建模,以形成一個自運作的物體,
面向服務編程有什么問題呢?不是一樣可以對服務內部建模嗎?
問題是當我們面向服務,以服務為核心載體時,往往做不到對其內部很好地建模,根本原因在于面向服務的外向型思維和面向物體的內向型思維是沖突的,我們建模時,只會有一種主導思維,其它只起輔助作用,當我們以服務為主導時,首先想的是功能,是做什么的問題;當我們以物體為主導時,首先想的是誰來做,是主體問題,一種是由功能推匯出功能提供者,此處提供者是輔體,功能是主體;一種是由主體推匯出主體行為,行為是主體的自在要素,
由于在面向服務編程中,提供者只是輔體,往往容易被忽略,我們并不是太在意誰提供功能,結果就是往往一個類提供了n多個功能,比如一個OrderService提供和訂單相關的一切功能,久而久之,Service(特別是業務中的核心Service如商城的OrderService和GoodsService)會變得例外臃腫,難以維護,混亂的職責分配還會導致業務邏輯泄露,不同的開發者可能會選擇由不同的提供者提供相同或相似的功能,導致一份業務邏輯在多處出現,
Service有其存在的必要性,但系統建模時不能面向Service本身建模,因為Service是粗粒度的,而應當面向物體(Entity),
那么,什么時候該用Service呢?想象一下這樣的場景:張三是一名PHP程式員,負責后端程式開發,但不熟悉前端技術(js、html等),現在有一項后端介面開發任務,顯然張三是能夠勝任的,現在又有一項網站開發的任務,由于張三不懂前端技術顯然無法獨自完成任務,需要前端工程師李四介入,現在問題來了,由于張三和李四是平級關系,誰也指使不了誰,作業無法開展了,咋辦?需要有新的協調者(如兩人的上司)介入,這個協調者就是服務,協調者本身不負責具體作業執行,而是負責協調、分工、調度以及外交,當網站開發任務中途發現還要其他角色加入(如運維),也是由協調者負責引入,后端、前端、運維只負責各自的作業,而不知曉其他人在做什么,甚至不知道其他人的存在(如三人各在三個國家),
服務不是必須的,并且不負責具體業務實作,服務的職責是在高層次上協調各物體的執行流程,并對外公布單一功能介面,
服務不是天生就存在的,是通過向上抽象來獲得的,當本層各物體之間的作業無法協調時,就需要向上抽取一個專門的服務,
例如銀行轉賬業務,涉及到收方賬戶和付方賬戶,收方賬戶收款,付方賬戶付款,但兩者是平行的,你不能說在收方賬戶的收款方法中呼叫付方賬戶的付款方法,反之亦不可,這時就需要更高層次的轉賬服務介入了,
當然,還有一種情況(另一個角度),當我們需要一個本背景關系以外的功能時(而我們又不想自己實作它),我們也稱之為服務,
關于服務命名:
我們目前存在大量以Service結尾的類命名,以此標識它是個服務,個人并不建議這種做法,原則上,我們并不能說“某某是一個服務”,而只能說“某某提供什么服務”,銀行提供借貸款服務,富士康提供代工服務,但我們不能說銀行和富士康是服務——它們屬于服務提供者,服務是功能,我們通常給予Service結尾的類實際是服務提供者,如“郵件服務提供者提供發送郵件的服務”,雖然平時我們都簡稱為“郵件服務”,但據此簡稱命名為
EmailService并不合適,更合適的做法是直接以服務提供者名稱命名,如Email,或者以該服務提供者主要提供的功能命名,如EventSchedular,據此名字我們知道它主要提供調度服務,而EventService到底是干什么的呢? -
面向資料庫編程 :
當我們接收到業務需求,大致分析完畢后,首先想要做的事情是什么?表結構設計,
是的,這是我們大部分人一貫的行事風格,甚至一個程式員技術水平高低全看他表結構設計的好壞,
但是我們冷靜地想想,資料庫對于業務到底意味著什么?它不過是資料的一種持久化方式而已,屬于基礎支撐層的東西,
面向資料庫編程的問題是,我們從一開始就鉆到最底層的細枝末節上,而不是以居高臨下的角度設計和審視業務系統內在的、本質的邏輯規則,這種設計方式,由于沒能深入地設計建模,很容易就事論事,往往迷失在細節森林中,另外,由于資料庫設計和業務建模是同時進行的,表設計中往往會帶進過多的當前業務邏輯,由于業務規則會隨著不同時期需求變化而變化,因而這種表結構是不穩定的,典型的體現是一個欄位表示多方面的狀態值,這些狀態之間遵守(目前的)固定的業務規則,資料庫設計不可能完全脫離具體的業務,但一定要盡量識別并減少不穩定業務規則的攝入,
面向資料庫編程思維是自始至終的,而不僅僅在初期表設計階段,編程程序中,我們習慣將增刪改查欄位這種資料庫術語帶進業務代碼中,操作也是直接面向資料庫進行的,
這種編程方式中,往往沒有物體,就算有,也是僅僅作為資料傳輸物件(DTO)使用(算不上DAO,因為連個CRUD的方法都沒有),這些物件往往沒有方法,其屬性基本和資料庫表一一對應,模型(Model)也淪為資料庫表的物件化表述(DAO),自帶CRUD,
面向資料庫編程之所以那么流行是因為簡單快捷,會敲代碼就能做,完全不用考慮物件建模,而且其弊端在簡單的業務復雜度面前并不會暴露,然而當業務復雜度達到一定規模后,其弊端是致命的,大量的面條式代碼牽一發而動全身,業務邏輯零散各地,需求迭代越頻繁,這些弊端就越早暴露,前期的大步流星扯得后面蛋疼不已,
要想深入地進行業務建模,必須在建模時忘掉資料庫,要意識到資料庫僅僅是持久化存盤的一種手段而已,這樣才能將你的思維從表結構中解放出來,深入到領域模型本身中去,
面向服務編程和面向資料庫編程正好是兩個思維極端:前者將全部注意力放在功能(行為)上,后者則將全部注意力放在資料上,
-
關于Repository(倉儲層職責以及事務該由誰負責) :
從分層上來說,倉儲處于業務邊界處,連接業務層和存盤層(基礎設施層),倉儲是懂領域語言的(但不代表要在里面實作具體的業務邏輯),另一方面它也懂持久化相關作業,在《實作領域驅動設計》(以下簡稱《實作》)一書中建議將倉儲當做集合使用,并將其定義(介面)放在領域層,將實作(實作類)放在基礎設施層,
我們先看看集合,集合里面裝著一組同質(同類)元素(物件),擁有添加(add)、移除(remove),注意,集合并沒有modify和save操作,因為我們獲取的是集合中物件的參考,當物件狀態發生變化時,集合中的那個物件是同步變化的,
《實作》中將倉儲分為兩類:面向集合的和面向持久化的,面向集合的倉儲實作嚴格模擬集合的行為,擁有add、remove方法,沒有save方法,因為我們獲取的是集合物件參考,其狀態的改變會立即反應到集合中(具體實作上內部會采用讀時復制或寫時復制機制來跟蹤物體狀態的變化),面向持久化的倉儲除了和前者一樣擁有add和remove方法,還有save方法,即外界需要顯示的呼叫倉儲的save(Object)方法來保存物體狀態的變更(實際中往往add也被合并到save方法中),兩者最大的區別在于是否顯示地保存物體變更,
無論是面向集合還是面向持久化,都要求我們以集合的方式來使用倉儲——這也正是倉儲和資料訪問物件DAO不同之處,倉儲是面向領域物件的,它的職責是將領域物件(聚合)持久化到基礎設施中,以及從基礎設施中獲取資料并還原成領域物件回傳,DAO是面向資料表的,一般和資料表一一對應,并自帶CRUD方法——和集合的add、remove方法不同,DAO的CRUD對應的是資料庫的CRUD操作的,
我們可以恰如其名地理解“倉儲”,它是一個倉庫,我們將貨物交給倉庫管理員,由倉庫保管,至于怎樣保管是倉庫內部的事了,另外我們根據貨物編號(以及其他過濾條件)向倉庫管理員索要指定的貨物,至于怎樣將貨物從倉庫中取出是倉庫內部的事,倉庫 可以自己決定如何保管貨物(如為了節約空間可能會將貨物拆卸分類存放),但倉庫必須保證取出來的貨物和當時放進去的是一模一樣的(除非不可抗原因如過期了),
我們現在的倉儲使用面臨一些問題,最大的問題是倉儲中包含了大量的業務邏輯——這也正是面向資料庫編程所導致的結果,我們的思維直接和資料庫打交道,而倉儲無疑是我們所撰寫的離資料庫最近的東西了,我們程式中并沒有領域物件(物體),因而和倉儲打交道的是毫無業務含義的陣列,
(PHP是成也陣列,敗也陣列,其陣列過于靈活強大(雖不見得性能多好),以至于一切都可以用陣串列示,面向物件也就變得毫無意義了,很多PHPER并不理解面向物件,認為其不過是將所有屬性設成private,然后不停地寫getter,setter,無聊透頂,吃力不討好,這種認知和長期主流框架(如spring)的機械導向有很大關系,
“如果一個類有20個屬性,我豈不是要寫20個getter和20個setter?”這是很多反對OO者喜歡舉的例證,事實是,你的什么類會有20個屬性?這多半是你的抽象出了問題(實際上這基本是在用面向資料庫的思維進行所謂面向物件編程,OO只是個空殼,而物件本身是和資料庫欄位一一映射的,才會導致這么多的屬性(資料表有20個欄位很正常),另外,OO本身(特別是DDD)并不提倡過度使用getter和setter,因為他們多半沒有具體的業務含義,試想一個public的setPrice()到底是什么意思?設定價格?什么樣的業務需要單獨setPrice?設計良好的類其屬性狀態一般是自維護的,而不是讓外界來set),
OO是一種思想,一種思考問題的方式,而在實作上,則應“合理的才是存在的”,不可機械搬套,
不是說PHP的陣列不可以用,而是說陣列應該作為“陣列”來使用,而不是萬金油,用陣列代替物件結構,會造成很大的維護復雜性,)
如果倉儲本身包含了大量業務邏輯,還不如使用傳統的(也是主流框架自帶的)Model,至少它有“模型”的概念(雖然是面向資料表的),而此處的倉儲則是“四不像”,
目前的倉儲使用的另一個嚴重問題是在倉儲中實作事務,從倉儲的描述來看,它只是擔任存與取的職責,何以跟事務掛鉤?這還是由面向資料庫編程的思維導致的,當提及事務,我們首先想到的是關系型資料庫中的事務,并且理所當然地認為此事務即彼事務,那么既然是資料庫的東西,當然要放到倉儲層了,
什么是事務?事務是指需保證一項任務的原子性,任務中的各項操作要么全部成功,要么全部失敗(撤銷掉),事務本身和資料庫沒半毛錢關系,我們說銀行轉賬業務需具有事務性,我們指的是這項業務,而不是業務背后的存盤技術,資料庫層面的事務是將“事務”這個概念應用到資料庫這個具體領域而言的,具體說來是事務中的一系列寫操作要么全部成功,要么全部失敗,我們真正需要關注的顯然是業務層的事務性,業務層不具有事務性了,存盤層的事務又有何意義?資料庫事務是對業務事務的低層技術支撐,改天我們不用關系型資料庫了,難道業務就不能有事務性了?
倉儲和物體的操作都是細粒度的,無法保證整體的事務性,也不應當知曉事務的存在,
事務應當放在那個能代表單一完整任務的方法中(如應用服務中),
將事務放在倉儲層,不可避免地會將業務邏輯帶入倉儲中,
-
關于Controller(控制器職責及權限系統初步論述):
控制器層是離用戶最近的層,在“埠配接器”中屬于配接器,和倉儲一樣(倉儲也屬于配接器),控制器也是“腳踏兩只船的”,一方面它懂用戶的輸入,另一方面它懂業務層的埠(介面),它將用戶的輸入資料轉換適配成相關業務埠所需要的資料,并將內層業務的輸出資料適配成用戶端所需要的資料,
控制器所代表的是用戶角度的一項任務(用例任務項),
控制器應該是很薄的一層,不應該有任何業務邏輯和流程控制,
控制器還有另一個功用:權限控制,權限用來控制用戶角度的一項任務能否執行,這和控制器正相呼應,控制器是側重于用戶角度的,它雖然知道用戶角度的一項任務應該交由領域中的誰(物體或服務)去執行,但其本身應當和領域層保持最大限度的解耦,
(用戶角度的一項任務可能需要跨業務領域的多個領域服務協作完成,此時應當引入應用服務進行跨領域協調,而不應該在控制器中進行協調,)
關于控制器和權限控制此處舉個例子:會員系統有魔力營銷、群發和素材管理幾個模塊,他們都需要新增和編輯圖文的功能,我們一般做法是三個地方都指向同一個url(同一個控制器的action)來編輯圖文,現在問題來了:如何進行權限控制?現在張三、李四、王五分別擁有(且僅擁有)魔力營銷、群發管理和素材管理的編輯權限,如何讓他們都能編輯圖文呢?一種做法是在圖文編輯的action中做這樣的權限控制:“需擁有魔力營銷或群發管理或素材管理的編輯權限”,如此繁瑣,日后再加一個呢?目前我們正是采用類似這種做法(在資料表中加各種跟路由相關的限制以及url中加各種引數,一堆東西搞得人云里霧里),
其實我們仔細思考就會發現,雖然三個地方都是編輯圖文,但它們的業務含義是不同的,并且此處僅僅是湊巧三個場景編輯圖文的操作是完全一模一樣的,就讓我們產生錯覺認為它們是同一件事情,從用戶角度(用例)來說,它們三件事情,編輯圖文是三件事情中的一個環節,它們可以碰巧完全一模一樣,但本質上是不同的(日后可以有不同的需求,如魔力營銷中編輯圖文時有額外的限制等),因而三個場景中的“編輯圖文”是三個用例任務,應該對應三個action(表現在url中是三個url,此處可能有人提出疑問:url作為統一資源定位符,代表了資源本身,難道同一個資源的url可以不同嗎?url只是代表了資源(的表述),并不是資源本身,資源與url是一對多的關系,就像一個事物可以有多個稱呼一樣(例如不同地方的人對紅薯的稱呼是不同的)),這三個action分別需要魔力營銷、群發管理和素材管理的編輯權限,
實際中,控制器所面向的用戶型別大致有這些:web(人)、console(命令列)、外部系統(api呼叫),控制器分別以web應用、后臺腳本、web服務的姿態呈現,上面談到的權限控制實際是屬于web應用中的業務角色鑒權,但各型別控制器可分別使用各型別的權限控制系統(例如我們常用的api服務呼叫時的賬號鑒權),
還有一點需要注意,我們說應該在控制器層進行權限控制,但這不代表說一定由控制器本身來實作,實際上,一般控制器并不直接執行權限控制,它甚至不知道權限系統的存在,一般我們會在一個統一的地方執行鑒權,但這個地方一定不是權限系統中,這里是在使用權限系統(權限系統的消費者),屬于權限系統外部,很多權限系統的設計犯了這種錯誤:將權限系統本身和對權限系統的使用混為一談,在權限系統中耦合了過多的消費者資訊(如選單、路由等),這里還需要澄清另一個事實:負責權限系統的團隊往往同時負責部分或全部權限系統消費者的維護,這就很容易將兩者糅合在一起,這樣的團隊一定要認真識別出哪些屬于權限系統本身,哪些屬于消費端,從而最大程度進行解耦,相較于消費端,權限系統應當是個相當穩定的存在,它不應當隨著消費端變化而變化(如消費端改變了選單、新增了路由等),
權限系統是什么?權限系統定義了某用戶是否擁有某項操作權限,如張三擁有群發管理權限,通常,為了維護的便捷性,權限系統都會引入角色的概念,這樣,不是給張三直接授予群發管理權限,而是創建一個群發管理員角色,給該角色授予群發管理權限,然后給張三賦予群發管理員角色,從而張三便間接擁有了群發管理的權限,
自此我們得出權限系統三要素:用戶、角色、權限集,其中角色、權限集還可以做分組處理,
(本文除非做特殊注明,否則都默認指業務權限系統)
對權限系統的使用是指:某項用例任務要求操作者(用戶)必須擁有某項權限,用例任務(或其代理)詢問權限系統,權限系統給予是或否的答復,
綜上,權限系統消費端如下使用權限系統:
- 消費端使用權限系統定義的權限集;
- 消費端詢問權限系統某用戶是否擁有某項權限;
回到上面魔力營銷的例子,首先在權限系統創建三個權限點:魔力營銷管理、群發管理、素材管理(具體視情況而定),然后創建推廣專員角色,并授予該角色魔力營銷管理和群發管理權限,然后給張三這個用戶賦予推廣專員角色,自此張三便擁有以上兩個權限點,魔力營銷和群發擁有兩個獨立的web控制器,這兩個控制器分別有一個editArticleAction(編輯圖文),兩者呼叫的領域層物件(如Article物體)是一樣的,但它們需要的權限是不一樣的:一個需要魔力營銷管理權限,另一個需要群發管理權限,
(這里視現實情況,可能將圖文編輯功能做成服務供遠程api呼叫,兩個action呼叫同一個ArticleService服務,該服務遠程呼叫圖文編輯服務)
有人表示不解:既然兩個都是編輯圖文,用一個單獨的編輯圖文的action不就行了嗎,干嘛搞兩個,僅僅為了權限控制?上文已說過,此處兩個編輯圖文是在兩個完全不同的業務場景中出現的,只是恰巧(目前)它們的操作是完全一樣的,哪一天需求變了,要求魔力營銷的編輯圖文操作需要一些額外的資料,你又如何在一個action中搞定?寫個if else?如果再后面需求變得完全不能使用同一套圖文編輯操作了呢?
我們往往被假象蒙騙,當我們發現兩件事物的名稱完全一樣,其行為表現也完全一樣時,便認為是同一個事物,初見歐洲人和美國人,因為第一眼發現的相似處便認為都是來自一個地方,系統設計中,一定要從業務本身去思考系統,深挖業務表象背后所隱藏的本質,
至此,我們發現了控制器作為用戶和業務系統之前橋梁的核心價值:用例層的行為和業務領域層的行為并不是一一映射的,因而需要控制器進行解耦與適配,
前面略微提到了應用服務,我們再次強調一下“服務”的特點:服務本身并不提供實作細節(在領域服務中,這是物體干的事),服務的主要職責是協調、調度下級單元,以及外交,服務的出現是由于下級之間不協調而產生的(即服務是自下而上抽取出來的),和領域服務是為了協調領域物體(以及其它領域服務)一樣,應用服務是為了協調多個限界背景關系(多領域),和領域服務不同,應用服務屬于應用層,不能包含領域業務,應用服務應當是很薄的一層,應用服務和控制器一起(其實控制器在這里也可以看成一個很簡單的應用層服務,而專門的應用服務則是為了處理較復雜的用例到領域模型的適配問題)構成應用層,作為用例(用戶)和領域模型之間的配接器,
理解應用層價值的關鍵是要認識到用例角度和領域模型角度是兩個截然不同的視角,看待問題的方式是不同的,產品經理描述業務時往往用的是領域模型視角(至少在DDD中要求這樣),而在人機互動設計時用的是用例視角,比如在博客詳情頁往往需要顯示最近評論和相關博客,這顯然屬于多個領域模型,如果認識不到這兩者的差別,設計出的領域模型往往會被用例模型牽著鼻子走,類便會違反單一職責原則,久而久之,代碼也就偏離的OO初衷,
技術上,用例模型我們一般用“展現模型”表示,比如Yii的FormModel,有時我們可以用DTO物件作為展現模型,從多個聚合中組建用例所需的資料,或者我們也可以直接針對用例在倉儲層查詢并組裝用例模型(用例優化查詢)——這聽起來有點怪,因為正常情況下,倉儲應當回傳領域層的聚合物件,而不應當回傳應用層的東西,這種做法基于CQRS思想:命令模型和查詢模型的阻抗失配,聚合一般是基于命令模型的,而用例是基于查詢模型的,
-
關于Entity :
前面多次提到物體,此處做下集中探討,
物體是DDD的核心,每個物體物件都有一個唯一標識以區別于其他物體,兩個物體間是否相等取決于它們的標識是否相同,兩個標識不同的物體,縱然所有屬性都相等也是兩個不同的物體,例如有兩個張三,性別、年齡都一樣,但他們仍然是兩個人,這點和值物件不同,兩個屬性完全相同的值物件是相等的,
物體有生命周期,在生命周期內物體的狀態是可以發生變化的,例如Order物體,在整個購買與售后程序中,訂單的某些狀態會發生變化,但無論怎么變,它還是同一個訂單,這點又和值物件不同,值物件是不可變的,
我們可以形象而簡單地理解為物體對應現實世界的“那個東西”(個體)(雖然這樣并不全面),一輛車,一潭訓分交易記錄,都是物體,物體有連續的生命周期,一輛車,從制造出來(new一個物件),到買賣交易(車的屬主欄位狀態發生變化),到上路跑(車的行程等屬性不斷發生變化),到報廢(物件被洗掉),雖然車的各種資訊不斷地在變化,甚至經過噴漆、改裝等變得“面目全非”,車還是那輛車,值物件就不一樣,值物件是不可變的,一旦屬性發生變化就變成另一個值物件,好比有個Address物件,其有city、stress屬性,city發生變化就會變成一個新的Address物件,相反,兩個Address的city和stress如果完全一樣,則認為兩個Address物件相等,
地址資訊是不是物體呢?關鍵看你的系統是否需要區分“那一個”地址,當兩條地址資訊完全一樣時(物件的屬性完全一樣),是否表示同一個地址呢?如果是,那它就不是物體,而是值物件——我們并不關注“那一個”,只關注它的值,
我們會發現,一個物體往往對應資料庫中的一條記錄,一般是這樣,但不完全一一對應,這里重點是不要用資料庫記錄來和物體一一對應,這樣很容易走向面向資料庫編程,謹記:資料庫只是資料存盤工具,
我們現在的代碼中偶爾也會發現Entity,但實際都是作為DTO或DAO使用的, 其欄位一般是和資料庫表一一對應,而且要么沒有方法,要么就是寫操作資料庫的方法,
Entity是我們建模的基礎和核心,在進行物體建模時,不要去考慮資料庫,而是要考慮現實世界,比如,設計積分系統時,有賬戶類Account,賬戶分為個人賬戶、公司賬戶和商家賬戶,在資料庫里面,所有的賬戶都是放在h_integral_account表里面,通過欄位區分是什么賬戶,以面向資料庫思維設計的話,我們也會有一個Account類,然后通過屬性type標識是哪種賬戶,這顯然是偽OO,用物件來模擬資料庫記錄(資料庫層面上,這種設計是合理的),如果我們全然拋開資料庫,思維就會從這種桎梏中解放開來,以OO方式來思考,賬戶分為個人賬戶、公司賬戶、商家賬戶,此處顯然是繼承關系,PersonalAccount、CompanyAccount和MerchantAccount繼承自Account,實際中,公司賬戶和商家賬戶有諸多相似處,那么可以再往上抽離出“對公賬戶”PublicAccount繼承Account,而CompanyAccount和MerchantAccount繼承PublicAccount,
我們再看看會員物體,
資料庫層面,會員資訊主要記錄在h_member表,該表有幾十個欄位,以面向資料庫方式設計物件的話,Member物件也會有幾十個屬性,然后幾十個getter和setter,然后——然后你們會千萬種吐糟OO如何如何不好用了,
從面向物件的角度出發,Member是一種身份,其更中性的存在是Person,作為人Person,只有幾種需要關注的屬性:姓名、身份證、性別、生日,會員Member是會員系統這個業務領域的核心概念,是Person在此系統中扮演的一種角色,
(“角色”這個概念在OO分析和設計中很重要,往廣義說,世間萬物之名皆是萬物之角色,一個事物在某時間點(或時間段)一定是以某一角色來從事某項活動(這也是四色原型分析的概述),搞清楚角色的重要性,就不會在程式設計中用一個Person(或Member)來代表一切用戶以及用戶活動,當用戶登錄時他是登陸者,當活動報名時是報名者,當發表文章時是作者,不同的身份有不同的屬性和方法),
會員作為一個核心角色,他應當有哪些屬性和方法呢?這里存在另一個陷阱:因為我們是會員組,開發的系統是會員系統,因而貌似一個Member可以搞定一切,
這里有兩種方案:要么直接廢棄掉Member這一說法(因為它太寬泛因而也太空洞了),直接用更具體的、細粒度的身份;另一種是對“會員”的概念進行嚴格定義,挖掘出狹義會員的概念,個人傾向于第二種方案,因為畢竟“會員”這個在業務領域是實打實的存在,直接在軟體模型中消抹掉會導致模型和領域不匹配,而且一些地方確實無法用其他概念代替,其實只要我們平時稍微注意下用于,就會發現實際上“會員”還是有比較明晰的邊界的,比如粉絲和會員就是兩個身份,
我們再看看業主,業主一定是會員(而不能僅僅是個粉絲),因而可以創建個Owner繼承自Member,注意這里的關系描述用的是“一定”,這種描述是“領域內必然性”,它不一定是亙古不變的,但這種基礎業務邏輯發生變更的幾率非常小,因而我們使用繼承關系,而不是其他關系(如聚合、組合等),相較于聚合和組合,繼承是更加穩定的關系,
(反過來,如果我們發現A和B僅存在不太穩定的“是一個”的關系,就要慎用繼承,而考慮其他關系(如用type標識其型別),)
另外我們看看Owner這個詞,一個詞的含義取決于背景關系,Owner的本意是“所有者”,比“業主”更為寬泛,
那么此處是不是要用RoomOwner呢?RoomOwner將我們的關注點帶到Room上,使得Owner不是很突出,而“業主”在會員系統是另一個核心概念,不應當將其命名依附在Room上,況且Owner本身即有業主之意,這里想說明的是背景關系對于名詞理解的重要性,以及核心概念要有核心名字,
最后再強調一遍:資料庫設計和物件設計是兩種完全不同的設計模式,
-
關于建模(模型/物體從何而來):
很多時候我們想去嘗試OO,卻苦于建模——要么腦海中半天搞不出一個模型出來,要么怕建出來的模型不符合業務,越搞越混亂,最后不可收拾,想來想去,還不如面向資料庫來得直截了當,
建模是一個不斷反復的解構與建構程序,
最初出來的模型總是很樸素、很不完善的,這屬于正常現象,因為此時你只認識了當前需求本身,還停留在現象層——而此時的模型恰如其分反映了這點,
隨著認知的深入(或編碼的進行——沒錯,編碼程序本身就是設計程序,會對模型進行精化、修正),原先樸素模型內部必然會暴露出一些矛盾點,這些矛盾點自然敦促你去進一步重新審視需求和模型,此時,你往往能透過需求現象看到業務領域的某些本質,
一定要認識到一點:(原始)需求屬于現象,不屬于領域本質,我們誠然要尊重現象,所建模型也要正確支撐現象,但現象不是本質,建模時不能完全圄于當前需求本身,
要不斷的回到需求本身,防止模型跑得太遠跑偏了,如果所建模型不能正確支撐需求了,那么需求和模型肯定有一方有問題(往往是模型出了問題),
當你在建模的路上“江郎才盡”了,要立馬回來讀讀需求,要么模型建完了,要么對需求還沒有完全理解,
要將需求書面寫出來,而不只是口頭概述,詳盡的寫出來(最好是多人一起深入討論,如果沒有條件,一人也行,后面可組織多人討論),在寫的程序中,你會不斷地發現新的概念,新的問題,實際上,這樣寫需求的同時就是在建模,DDD強調需求分析和模型設計的語言一致性,
寫需求檔案時,需要注重概念的提煉,由于平時需求描述的不規范性(或需求者本身的概念模糊性——需求提出者往往不是領域專家),原始需求一般會夾雜各種干擾因素,比如在所有場合都使用“用戶”來代表系統使用者,將活動報名人和參加人混為一談等,如果基于原始需求建模,你會發現整個系統就一個超級的User類,需求分析不但是潛藏概念挖掘程序,還是已知概念的“正名”程序,“粉絲”是什么,不是什么,“會員”是什么又不是什么,他和“粉絲”又是什么關系等,
記住:建模的開端不是畫類圖,而是寫需求分析檔案,你的類圖應當和需求分析檔案大體保持一致,當兩邊有很多概念或邏輯上的不一致時,說明模型有問題,
[ 注意:原始需求和經過我們自己分析后的需求是有區別的,詳細分析后的需求一般是“現象與本質的統一”, 具體體現上是需求分析/設計檔案和領域模型的統一,注意這里分析和設計是一體的,]
另外需要注意:需求分析完成了只代表最核心的模型完成了,不代表建模作業本身完成了,
需求分析是迭代的,程式設計、建模也是不斷迭代的,從個人實踐來看,即使是一個專案的初版開發也會經歷幾次大的內部重構,而那些所謂在實際編碼之前必須畫出詳盡 UML 圖的,純屬理性主義烏托邦,
-
關于framework/sdk/vendor (康威定律如何體現在framework的內部矛盾中):
康威定律 :設計系統的組織,其產生的設計等同于組織之內、組織之間的溝通結構,
framework的產生基本離不開“早期團隊”,當我們發現兩個獨立的團隊使用同一個framework程式時,基本能斷定他倆曾經在一起過(或者有共同的直接上司),
由于一些功能需要各模塊或專案之間公用,我們便將它放到一個單獨的目錄中,然后在各專案中參考,這在一個小型團隊中是沒問題的,但是隨著業務的增長,團隊會分化成多個獨立的團隊,此時,各團隊之間公用的framework便出現所有權問題,公用的代碼要么沒有人維護,要么都來修改,還有另一個問題,雖然各團隊現在分開了,但各自仍然認為那個framework是自己的,自己團隊的公用代碼仍然應該放在那里面,于是此時framework里面便充斥著各個團隊的“公用”代碼,雖然這些代碼對其他團隊幾無用處,
就敏捷型團隊來說,framework并不是很好的公用代碼形式,framework比較適合傳統的團隊,這種團隊隨著業務拆分往往會采用團隊內分組而不是分成完全獨立的團隊,敏捷型團隊講究小型和自管理,團隊之間的溝通并不是很頻繁,因而在系統架構上也應該與之適配,相互之間的代碼盡可能解耦,
更好的方式是采用composer形式,每個功能作為獨立的composer,每個composer包有明確的所有者,整個公司有一個私有的composer倉儲,每個團隊管理自己的包(甚至是自己的倉儲),其它團隊可以使用,但不能修改,如果其它團隊覺得需要針對他們團隊做較大修改,可以fork一個獨立分支自行維護,
對package的要求:
- 只提供比較單一的功能,對其修改不會導致大面積輻射;
- 面向介面編程,盡可能保證其對外穩定性,甚至可以在團隊間制定契約(類似PHP-FIG成員間為統 一編程規范而制定的一系列PSR規約一樣);
- 應當遵守開源社區的標準版本命名規范,使用者可以選擇使用不同的版本;
- 可以追溯哪些專案安裝了該package,當發生版本變更時可以進行相應通知,
- package不僅僅是用來提供通用功能,還可以用來定義契約(介面),即該package只提供介面定義,不提供實作,其他的package或專案可為之提供實作(這可以在團隊間制定契約),
有人擔心這種方式會帶來升級的復雜性:升級package需要通知所有使用者進行專案更新,首先package應該是比較穩定的,如果一個package需要頻繁修改,要么是實作有問題,要么是它做得太多了;而且并不是每個package都是被很多團隊大量使用,也不是每次升級都是必須的;另外,如果能追溯哪些專案使用了這個package,則針對必須進行的升級可以按需通知,最后,framework模式同樣存在升級問題,而且由于是一次升級必須升級所有功能,會帶來更大的安全隱患,
需要時刻注意一點(無論是package模式還是framework模式):哪些功能應當以服務的形式而不是package(或framework)的形式提供,我們使用package直接目的是提取多個專案要用到的功能以公用,但很多時候這些功能以服務的形式提供更適合,比如積分,每個專案都要用到,但相比于做成package,更適合做成獨立的積分系統供其他系統呼叫,再比如公眾號相關的功能也應該以服務而不是framework來提供,
目前我們采用sdk來替代framework,其實是換湯不換藥,各團隊之間有個公用的sdk,由公共團隊維護,然后各團隊各自有個自己的sdk,其本質還是framework,即如果哪一天公共團隊不存在了,公共的sdk便成為麻煩;哪個團隊分裂成多個團隊了,那個團隊的sdk便成為麻煩,現在之所以沒有出現問題是因為云服務目前的組織架構并沒有發生很大的變動,
framework(或sdk)問題的本質原因在于其強耦合的代碼架構和松耦合的敏捷團隊之間的矛盾,framework是非常粗粒度的技識訓分,里面的代碼之間可能沒有任何關系,僅僅因為它們都是“公用代碼”就走到一起,更糟糕的是,很多領域業務,因為多個專案需要用到而跑到了framework中(本應當抽離成獨立的服務),
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/53128.html
標籤:PHP
上一篇:基于 Redis 的訂閱與發布
