(4) 基於領域分析設計的架構規範 - 充血模型之實體

本系列目錄:java

  1. 改變與優點
  2. 領域分析基礎
  3. 讀寫隔離
  4. 充血模型之實體
  5. 充血模型之Service
  6. 關於重構與落地

讀寫隔離後的世界

基於上面提到的讀寫隔離的思想,那麼咱們能夠很清楚地看到上面這種狀況能夠看到:數據庫

世界觀

查詢業務,從入口層(如Controller),調用Finder,而Finder調用Repository(具體實現如Hiberante,Mybatis等等都可),這一條線下來,咱們全然不用考慮這個系統的增刪改就是如何作的,就像他們徹底處於不一樣的空間同樣,互不干涉,互不影響,甚至,永遠互不相見。 某種程度上來講,這種這種架構追求的效果,一種美感。安全

因此,接下來,咱們關注的,就是增刪改這一部分了,也就是命令操做 是開始要紮紮實實地來對這個系統進行修改了架構

首先讓咱們把視野擡高一些,從整個項目產品的上空來看看app

業務靈魂-狀態圖

除開少數很是扁平的純技術服務項目(好比AI識別,文本分析等等),其餘絕大部分企業項目都有其核心的商業邏輯,而這些邏輯,每每也會以核心的領域概念來提現,從簡單粗暴一點角度映射到設計開發中,那就是類class框架

  • 電商系統,核心領域至少有【商品】,【訂單】,【用戶】,【物流】等;
  • SNS社交平臺,至少有【用戶】,【博文/帖子】,【私信】,【通知】等;
  • 進銷存系統,至少有【帳戶】,【角色/權限】,【商品】,【客戶/供應商】等;
  • 在線教育平臺,至少有【用戶】,【課程】,【訂單】等;

而這其中,也有主次,你們能夠回頭看看本身所開發過的項目。 但凡有狀態字段的類,很大可能都是整個項目的核心領域之一。 其實很好理解,由於它有流程,由於它須要被各種操做來變動它的狀態,因此,他極可能貫穿了這個項目中某一個關鍵商業邏輯,好比電商系統中,編碼

  • 【訂單】確定有狀態,從[待支付]-[已支付]-[派送中]-[已收貨],甚至還有[已取消],[退款中],[退款失敗],[退款成功],腦補一下,就知道會生成多少複雜的業務流程了
  • 【用戶】通常來講也會有狀態,好比[正常],[凍結],可是能夠想到,若是某個系統沒有這方面權限與安全的要求,【用戶】也可能就沒有狀態了,那麼天然也不會有對應的操做對其進行修改,可能只會有建立

因此,若是在這些系統的早期設計階段,要我選擇一個最重要的UML圖,我會選擇狀態圖,如下就是整個系統中最核心的訂單狀態圖.net

支付狀態圖

能夠看到,把握一個核心領域的狀態變動,天然而然就能概括出來很大一部分系統的功能需求。咱們在這裏看到,這些全部箭頭所觸發的動做,其實都是命令,也其實都是會落地到各個相關領域的增刪改上。設計

固然這裏仍是一個粗粒度的表示,沒法單單依據這個就立刻落地開發,由於即便每個箭頭所表明的功能均可以寫出一個完整甚至很複雜的用例。但至少這是一個很是清晰的引導。code

貧血模型的世界

咱們目前所用的Spring體系,幾乎都是貧血模型,也就是說,真正的實體類裏,都只有各個屬性的Get與Set方法。 而假如咱們要進行一個操做,訂單取消,那麼最多見的作法是什麼?

//一個大而全的訂單服務類
public class OrderService{
   public void cancelOrder(Long orderId){
       Order order = orderRepository.getById(orderId);
       order.setStatus(OrderStatus.CANCELLED);
       //省略,其餘屬性的操做...
   }
}

//而後在上層(如Controller層)中這麼調用
orderService.cancelOrder(10086);

這是目前行業中很是流行的作法,也是Spring的IOC機制自然造成的作法————儘量的無狀態化。這種作法,在業務迭代時對代碼的變更評判標準相對簡單,都往Service裏放就好了,而後實體對象只須要GetSet便可,簡單粗暴,很是容易上手,也正是這種特性,讓這種編碼風格廣爲流傳。

以上這些話沒有任何貶義,由於任何事情,存在即合理,我所經歷的公司項目,幾乎都是這樣作的,你們合做起來沒多大問題,業務也都還跑得不錯。

那爲何我還想去作一些改變呢?

實體Entity的世界

由於我以爲咱們須要再從新審視一下實體Entity

實體爲何要有主鍵? 由於沒有主鍵,那咱們怎麼知道時要查詢/修改哪條數據呢?

這個回答沒有問題,只是這句話裏其實還蘊藏更深的含義

  1. 這個實體是一個真實存在的東西(對,哪怕它看不見摸不着,但也是存在的),並且會以一種形態被「存儲/持久化」在一個存儲介質裏,好比說數據庫;
  2. 當咱們須要對某個實體進行操做時,咱們須要經過一種手段將它「加載/讀取/得到」出來,就像你取快遞時,快遞員根據你提供的編號,從包裹裏把那個東西取出來,徹底同樣;
  3. 取出來了怎麼辦?那天然就是要對它進行操做了。沒錯,這個操做,就是對咱們找出來的實體進行操做,而不是別的東西。

因此,從「拿取」,到「操做」,這兩步,一切瓜熟蒂落,行雲流水,因此,以領域驅動設計的作法,或者說,充血模型的作法,會是這樣:

//應用層入口類,這裏以Controller爲例
public class OrderController{
   
   @PostMapping("/cancel")
   @Transactional
   public ActionResponse cancelOrder(@RequestBody CancelOrderRequest request){
   
       //拿取:根據標識符定位到咱們要操做的實體
       Order order = orderRepository.getById(request.getOrderId());
	   
       //操做:對,沒錯,說的就是你 order,就是對你,進行操做,不是別人!
       order.cancel();
	   
       //返回結果
       return ActionResponse.ok();
   }
}

//真正的業務邏輯,就是在Order實體裏
@Entity
public class Order{

	private OrderStatus status;
	private String customerName;
	//...
	
	public void cancel(){
	    //變動狀態
	    status = OrderStatus.CANCELLED;
	    //一些其餘屬性變更,略
	}
}

好,依舊有很多值得探討的地方:

  1. 咱們這裏直接在Controller中就開啓了Transactional,可能看起來有點反常規,但我我的以爲沒什麼問題,除了有點不習慣,仔細想一想,自己都只不過是Spring的一種組件而已
  2. 因此若是你用的諸如Hibernate之類的JDBC框架,能夠無需再進行多餘的相似save操做,這也更好的提現了領域設計的思想,由於這時,這個order就是一個實實在在被咱們找出來的實體,對它的改動,自動映射到底層持久化,很天然,也必然。

最更容易引起槽點的地方,就是order.cancel(),也就是充血模型的精髓,將行爲定位到一個實體類上,而不是不加思考地直接扔進OrderService裏。

業界一直有一種很是「美妙」地說法,曾經我一度很是嚮往,就是「讓代碼成詩」。 換句話說,就是既然追求可讀性,那麼咱們要儘量的讓代碼自然具備一種「主謂賓」的感受,就拿上面「取消訂單」作比方,咱們是否會以爲:

訂單好端端的在那裏放着,它本身又不能對本身作什麼,天然應該「別人」對他進行了操做:

OrderService.cancel(orderId);
  某某某      取消了 這個訂單

Perfect! 這樣讀起來,才很是通順,可讀性才更好!

我曾經也是這種風格死忠,而Spring廣爲流傳的無狀態架構模式也將這種風格發揚光大。 只是我如今,在經歷了愈來愈多複雜業務,長事務的開發需求後,愈來愈以爲,這個還有有些硬傷

  • 若是必定要讀得通暢,更應該是someOperator.cancel(orderId)即某個操做人取消了訂單,而不是OrderService,誰都知道OrderService就是一個無狀態的代碼大集合,一個冰冷的代碼而已。但顯然someOperator.cancel(orderId)這種作法也是更加不可能實現的,緣由就不用過多解釋了。

  • order.cancel(),只有2個部分,{操做目標是誰}.{作了什麼事情},清晰明瞭,言簡意賅。我相信絕大多數人的閱讀習慣也都是從左往右,那麼視線第一下掃到的目標必定是最左邊的執行對象,也就是order,那麼能夠在第一時間明確,這個行爲是發生在誰身上,而若是是orderService.cancel(orderId),無形中,orderService是一個佔據了視線最有力位置的一個巨大的噪點——由於它沒有任何的業務意義,你要看的,反而是後面的方法和參數,這在閱讀上百行甚至幾百行的複合長業務的時候,你會很快困頓,迷失方向。不少時候,咱們真的不是技術不達,而是身心疲憊

下一篇 充血模型之Service

相關文章
相關標籤/搜索