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

本系列目錄:java

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

Entity與Service,相愛相殺

好,接上一篇。sql

既然採用order.cancel()這種模式,那麼一個新的問題來了:安全

全部的命令操做都要變成這樣子嗎?那曾經巨大的OrderService的代碼,豈不是隻是單純挪了一個位置,放在Order裏面了,除了上面所謂的可讀性的優點,那還有什麼用?mybatis

並非,只是一部分放在實體類,其他的命令操做,依舊會採用一種Service來作。 因此,咱們必然須要一個能夠清晰量化的規範,來肯定這些行爲該放在哪裏:app

若是一個命令操做,只修改了一個聚合對象內部的相關數據,那麼,就歸屬給這個聚合 好比,訂單取消這個行爲,須要作的事情有:框架

  1. 訂單狀態標記爲取消
  2. 訂單變動記錄插入一條,「訂單取消」

根據咱們以前的圖能夠知道,這些修改操做,都在這個訂單聚合內,很天然的歸屬給order分佈式

訂單聚合

注意,咱們反覆強調了這裏是「修改操做」,也就是說,若是須要咱們在此操做期間,查詢其餘聚合的信息,只要不作修改,那就是容許的!就像下面這樣:工具

@Entity
public class Order{

  private OrderStatus status;
  private String customerName;
  private User orderCreator;   //假定這裏是下單用戶,省略了many-to-one的配置
  //...
  
  public void cancel(){
  
      //修改操做1:變動訂單自身狀態
      status = OrderStatus.CANCELLED;
      
      //查詢用戶信息,要記錄到訂單變動日誌中,
      //這裏若是是Hibernate,會直接觸發sql查找,若是換成其餘如mybatis,則用對應的repository操做便可,總之,是一個純查詢
      String userName = orderCreator.getUserName();
    
      //修改操做2:增長一條訂單狀態變動信息,具體實現省略
      createOrderTrack(OrderStatus.CANCELLED,userName);
  }
}

而後,就要從另一個角度來講了 若是一個命令操做,而且要求是一個完整的事務,修改了多個聚合的數據,那麼,須要爲這個行爲創建一個 Service 而這個Service,不會是一個{領域名稱}+Service,而是一個{具體動做}+Service,好比OrderPayService,訂單支付,假定有以下動做:優化

  1. 訂單狀態改成支付中
  2. 商品庫存對應扣減
  3. 用戶若使用了優惠券,則優惠券標記爲使用中 這幾個操做,是要在一個完整的事務中的,因此咱們寫在一個Service中
@Transactional
public class OrderPayService{                            //-----------(1)

    @Autowired OrderRepository orderRepository;          //-----------(2)
    @Autowired CouponRepository couponRepository;

    public String execute(Long orderId,Long couponId){   

        //暫不考慮前置狀態檢查

        //訂單屬性變動
        Order order = orderRepository.getById(orderId);
        order.setStatus(OrderStatus.PAYING);

        //商品庫存扣減,按以前的假定,一個訂單隻對應一個商品
        Prodect product = order.getProduct();
        product.minusStock(order.getQuantity);           //-----------(3)

        //變動優惠券狀態
        Coupon usingCoupon = couponRepository.getById(couponId);
        usingCoupon.setStatus(CouponStatus.USING);

        //去交易中心獲取支付unikey
        CreatePayResponse payResponse = payCenterApi.createPay(...各類參數...);
        return payResponse.getUnikey();
  }
}

//這時,上層入口(如Controller)就是這樣調用了
orderPayService.execute(orderId,couponId);

好,老規矩,深刻探討一下:.net

  1. 類被命名爲訂單支付服務,也就是{一個動做}+服務,代碼的清晰性上來講不言而喻,但也意味着一個操做就要有一個service,其實這是很是符合單一指責原則的,可是確定會有很多同窗以爲這樣作是容易產生過多的類,過分設計了。確實,會有這種狀況,但我依舊推崇這樣作,或者說,若是必定要一個service裏多個行爲,那至少表示這個行爲是相關的好比都是訂單,可是PC端下單APP下單等等,能夠放在一塊兒,這樣職責不氾濫,也便於代碼複用
  2. service,咱們能夠給與其足夠的權限,只要它須要,它能夠無所顧忌地獲取全部的上下文組件,無論是jdbc組件,仍是外部rpc組件,都是能夠的。由於給它的定義,原本就是多聚合的事務處理類,因此,只要它能保證事務的安全性,保證業務的完整,這一切都是沒問題的(這裏暫時不討論分佈式事務問題,那是另外要一個議題)
  3. 庫存扣減,咱們這裏採用了product.minusStock(quantity),而不是直接對product進行屬性修改。固然,直接進行屬性修改也是可行的,可是爲什麼這裏卻封裝成了一個方法呢?極可能的緣由是,最先的時候,是直接改屬性的,但後來有不少地方都要扣減庫存,因此,代碼重構了,而後minusStock應運而生。關於,重構,咱們後面還會提到。

一個特別注意的點

再思考一個常見的例子:帳戶轉帳

應該是這樣 account.transfer(otherAccount)嗎?

這裏,會有點爭議,但我會提倡用AccountTransferService來作,由於雖然這個操做從宏觀上來講是屬於一個領域聚合,可是,這個操做,倒是徹底不一樣的對象! 是同時修改了兩個對象的數據,並且天然要求完整的事務性。

因此,這種場景,也能夠理解爲,Service,是處理跨聚合對象


關於實體的Set方法

若是咱們使用不少ORM框架,因爲框架的實現策略的緣故,實體類是須要把全部的Get和Set方法都要開放的,並且上面你們也看到當咱們用OrderPayService的時候,也直接使用的對象的setXXX方法,因此Set天然更加須要開放了。

但對於set,本文這套規範,極力倡導一個原則:在進行業務開發時,Set能調用的地方只有1個,那是就在service中! 其他的任何場景,任何地方,都不容許(或者不必)調用set方法,尤爲是下面這種場景:

//在一個上層,好比Controller中
@GetMapping("/coupon/disable/{id}")      //失效某張優惠券,偷懶就不用Post了
public ActionResponse disableCoupon(@PathVariable("id") Long id){
      Coupon coupon = couponRepository.getById(id);
    
      //錯誤,禁止!!!
      coupon.setStatus(CouponStatus.DISABLED);
    
      //正確的應該是
      coupon.disable();--------------------------
}                                               |
                                                |
public class Coupon{                            |
                                                |
  private CouponStatus status;                  |
                                                |
  public void disable(){  <-------------------- |
      status = CouponStatus.DISABLED;
  }
}

必定會有同窗立刻提出疑問

才一行代碼,爲何不能直接用set?強迫症嗎?

不否定,這個規範,的確有點強迫症,可是真的是有好處的。

領域設計的思想裏,嚴格意義上來講,Get和Set都是不能隨便暴露的,尤爲是Set,是在修改這個系統,是有必定風險與危害的,那麼,任何一個set,都必定是有緣由的,必定是要歸屬到一個具體的業務命令操做中的。

其實,我在思考這套規範期間,一度將set方法直接設置成本包可見的級別,但願經過Java編譯報錯來杜絕這種狀況,可是這樣又和上面提到的service的模式出現了衝突,最終只能做罷。

工廠

到此爲止,咱們能夠承認,全部的命令操做,都將會歸類到Entity或者Service中

但有一個特例,這裏有必要提出來單獨說一下:一個實體的建立,也就是增刪改查中的 增 的操做

由於刪,改的操做,都是先找到一個實體,而後進行操做。但建立卻不一樣,由於在執行建立操做以前,這個實體都是不存在的,你怎麼找?就更加不可能有相似 order.create(params)這種代碼出現了,order尚且不存於世呢! 因此,這裏,天然的想到經過建立OrderCreateService來處理,但考慮到新增的特殊性,建議直接用工廠模式來作,即OrderFactory

對於OrderFactory,它的責任並不是簡簡單單new Order()而後一堆setXX後完事。詳細說明以下:

  1. 負責建立聚合根對象,好比訂單聚合中的Order,每每建立會有諸多不一樣的場景,好比建立一個空對象,或者建立有不少默認組件的對象等等,這個就根據業務場景來了。總之返回值必定是一個新建立的實體類。
  2. 負責建立聚合中其餘對象,可是這種場景仔細想來,並不會太多。由於聚合中其餘「附屬實體」的建立每每會以聚合根實體的某一個命令操做相關。好比訂單變動記錄OrderTrack的建立每每是伴隨Order的各類各樣的操做,好比訂單建立,支付,發貨,取消等等,而大部分時候不須要單獨出現諸如OrderFactory.createOrderTrack的狀況。
  3. Factory中原則上是容許觸發對其餘領域聚合的數據變動的,由於它是一個特殊的領域Service。但從通常的業務場景來講,這種狀況並很少見,由於建立後當即變更某個其餘領域的數據,每每會直接在應用層加入代碼,或者經過事件來處理。

事件

領域事件,在最先的《領域驅動設計》一書中,並未說起。在以後的相關書籍,諸如《實現領域驅動設計》中,有將其做爲一等公民的身份進行詳細講解。很慚愧,這一塊我一直沒有GET到其精髓,因此我只是結合更廣義上的事件,來作了一些分析,若有不合適的地方,也歡迎各位拍磚。

說到事件,你們必定能聯想到事件的廣播,事件的處理。沒錯,事件是一個很是好的解耦和工具,也是一個很是舒服的「梳理工具」,由於在討論需求的時候,常常可以看到很是「瓜熟蒂落」的事件場景描述:

當用戶建立了一個訂單後,要同時生成一個訂單變動記錄 ------ (1)
當用戶的訂單支付成功後,要同時爲這個用戶生成一個XX獎勵券,而且用戶活躍積分+10   ------ (2)

這一些,都太符合「事件」了,這些有些是在項目初版的時候就清楚了,有些則隨着版本迭代不斷加入。 可是回過頭的仔細想一想,若是遇到這種需求場景的時候,你們在實際的開發中,都真的用了事件嗎?不管是單機應用仍是分佈式應用,咱們在不加入事件機制的前提下,上面這些功能經過直接「調接口」都是徹底能知足的。

沒錯,因此對於事件模式,咱們能夠倡導一個原則:全部的事件,從重構中得來

從重構中得來,意味着,咱們不必在需求一開始就大量採用事件的作法,即便需求描述中有「當/若是...就...」,由於絕大部分時候,咱們每每沒法得知以後的產品的發展方向是什麼,過早的事件設計(尤爲是分佈式系統)會給代碼的閱讀流暢性,事務管理等等帶來更大難度。

事件的優點在於一處廣播,多處接收,因此,當「接收方」愈來愈多,事件機制的優點也越能體現出來。因此,我認爲最佳的實踐方式,或者更容易推廣的實踐方式,仍是跟着版本迭代來不斷優化代碼,在逐漸清晰地產品發展方向和擴展方向上,將原有的「直接調用」轉變成「事件處理」。通常來講,當「接收方」出現2-3個的時候,能夠開始考慮轉變成事件機制了,好比上面的(2)

固然,這裏並不否定在第一時間就加入事件機制的作法,只是建議若是肯定要在一開始就這樣作,但願這種作法的開發負責人務必對業務的擴展方向有足夠清楚的認識與瞭解。(好比一家公司在原有系統上開發新的升級版系統,這個時候,能夠在第一時間就作好設計優化,由於有很好的業務背景基礎)

至於具體的代碼實現方式,在單機應用中,Spring有很好的事件機制,並且可以支持事務的完整性。而分佈式系統中,更多的接用消息中間件來實現,具體技術細節就不更多展開了。

下一篇 關於重構與落地

相關文章
相關標籤/搜索