(5) 基于領(lǐng)域分析設(shè)計(jì)的架構(gòu)規(guī)范 - 充血模型之Service

Entity與Service洋机,相愛相殺

好坠宴,接上一篇。

既然采用order.cancel()這種模式绷旗,那么一個(gè)新的問(wèn)題來(lái)了:

所有的命令操作都要變成這樣子嗎喜鼓?那曾經(jīng)巨大的OrderService的代碼,豈不是只是單純挪了一個(gè)位置衔肢,放在Order里面了庄岖,除了上面所謂的可讀性的優(yōu)勢(shì),那還有什么用角骤?

并不是隅忿,只是一部分放在實(shí)體類,其余的命令操作启搂,依舊會(huì)采用一種Service來(lái)做硼控。
所以,我們必然需要一個(gè)可以清晰量化的規(guī)范牢撼,來(lái)確定這些行為該放在哪里。
好的疑苫,那就來(lái)詳細(xì)說(shuō)明一下規(guī)范:

如果一個(gè)命令操作撼短,只修改了一個(gè)聚合內(nèi)部的相關(guān)數(shù)據(jù)禾嫉,那么,就歸屬給這個(gè)聚合
比如蚊丐,訂單取消這個(gè)行為熙参,需要做的事情有:

  1. 訂單狀態(tài)標(biāo)記為取消
  2. 訂單變更記錄插入一條,“訂單取消”

根據(jù)我們之前的圖可以知道麦备,這些修改操作孽椰,都在這個(gè)訂單聚合內(nèi)昭娩,很自然的歸屬給order

訂單聚合

注意,我們反復(fù)強(qiáng)調(diào)了這里是“修改操作”黍匾,也就是說(shuō)栏渺,如果需要我們?cè)诖瞬僮髌陂g,查詢其他聚合的信息膀捷,只要不做修改迈嘹,那就是允許的!就像下面這樣:

@Entity
public class Order{

  private OrderStatus status;
  private String customerName;
  private User orderCreator;   //假定這里是下單用戶全庸,省略了many-to-one的配置
  //...
  
  public void cancel(){
  
      //修改操作1:變更訂單自身狀態(tài)
      status = OrderStatus.CANCELLED;
      
      //查詢用戶信息秀仲,要記錄到訂單變更日志中,
      //這里如果是Hibernate壶笼,會(huì)直接觸發(fā)sql查找神僵,如果換成其他如mybatis,則用對(duì)應(yīng)的repository操作即可覆劈,總之保礼,是一個(gè)純查詢
      String userName = orderCreator.getUserName();
    
      //修改操作2:增加一條訂單狀態(tài)變更信息,具體實(shí)現(xiàn)省略
      createOrderTrack(OrderStatus.CANCELLED,userName);
  }
}

然后责语,就要從另外一個(gè)角度來(lái)說(shuō)了
如果一個(gè)命令操作炮障,并且要求是一個(gè)完整的事務(wù),修改了多個(gè)聚合的數(shù)據(jù)坤候,那么胁赢,需要為這個(gè)行為建立一個(gè) Service
而這個(gè)Service,不會(huì)是一個(gè){領(lǐng)域名稱}+Service白筹,而是一個(gè){具體動(dòng)作}+Service智末,比如OrderPayService,訂單支付徒河,假定有如下動(dòng)作:

  1. 訂單狀態(tài)改為支付中
  2. 商品庫(kù)存對(duì)應(yīng)扣減
  3. 用戶若使用了優(yōu)惠券系馆,則優(yōu)惠券標(biāo)記為使用中
    這幾個(gè)操作,是要在一個(gè)完整的事務(wù)中的顽照,所以我們寫在一個(gè)Service中
@Transactional
public class OrderPayService{                            //-----------(1)

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

    public String execute(Long orderId,Long couponId){   

        //暫不考慮前置狀態(tài)檢查

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

        //商品庫(kù)存扣減由蘑,按之前的假定,一個(gè)訂單只對(duì)應(yīng)一個(gè)商品
        Prodect product = order.getProduct();
        product.minusStock(order.getQuantity);           //-----------(3)

        //變更優(yōu)惠券狀態(tài)
        Coupon usingCoupon = couponRepository.getById(couponId);
        usingCoupon.setStatus(CouponStatus.USING);

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

//這時(shí)代兵,上層入口(如Controller)就是這樣調(diào)用了
orderPayService.execute(orderId,couponId);

好纵穿,老規(guī)矩,深入探討一下:

  1. 類被命名為訂單支付服務(wù)奢人,也就是{一個(gè)動(dòng)作}+服務(wù),代碼的清晰性上來(lái)說(shuō)不言而喻淆院,但也意味著一個(gè)操作就要有一個(gè)service何乎,其實(shí)這是非常符合單一指責(zé)原則的句惯,但是肯定會(huì)有不少同學(xué)覺得這樣做是容易產(chǎn)生過(guò)多的類,過(guò)度設(shè)計(jì)了支救。確實(shí)抢野,會(huì)有這種情況,但我依舊推崇這樣做各墨,或者說(shuō)指孤,如果一定要一個(gè)service里多個(gè)行為,那至少表示這個(gè)行為是相關(guān)的比如都是訂單贬堵,但是PC端下單恃轩,APP下單等等,可以放在一起黎做,這樣職責(zé)不泛濫寝凌,也便于代碼復(fù)用
  2. service裕偿,我們可以給與其足夠的權(quán)限,只要它需要,它可以無(wú)所顧忌地獲取所有的上下文組件萎馅,不管是jdbc組件,還是外部rpc組件命咐,都是可以的壹堰。因?yàn)榻o它的定義,本來(lái)就是多聚合的事務(wù)處理類爬骤,所以充石,只要它能保證事務(wù)的安全性,保證業(yè)務(wù)的完整盖腕,這一切都是沒問(wèn)題的(這里暫時(shí)不討論分布式事務(wù)問(wèn)題赫冬,那是另外要一個(gè)議題)
  3. 庫(kù)存扣減,我們這里采用了product.minusStock(quantity)溃列,而不是直接對(duì)product進(jìn)行屬性修改劲厌。當(dāng)然,直接進(jìn)行屬性修改也是可行的听隐,但是為何這里卻封裝成了一個(gè)方法呢补鼻?很可能的原因是,最早的時(shí)候雅任,是直接改屬性的风范,但后來(lái)有很多地方都要扣減庫(kù)存,所以沪么,代碼重構(gòu)了硼婿,然后minusStock應(yīng)運(yùn)而生。關(guān)于禽车,重構(gòu)寇漫,我們后面還會(huì)提到刊殉。

關(guān)于Entity的Set方法

如果我們使用很多ORM框架,由于框架的實(shí)現(xiàn)策略的緣故州胳,實(shí)體類是需要把所有的Get和Set方法都要開放的记焊,而且上面大家也看到當(dāng)我們用OrderPayService的時(shí)候,也直接使用的對(duì)象的setXXX方法栓撞,所以Set自然更加需要開放了遍膜。

但對(duì)于set,本文這套規(guī)范瓤湘,極力倡導(dǎo)一個(gè)原則:在進(jìn)行業(yè)務(wù)開發(fā)時(shí),Set能調(diào)用的地方只有1個(gè)瓢颅,那是就在service中! 其余的任何場(chǎng)景岭粤,任何地方惜索,都不允許(或者沒必要)調(diào)用set方法,尤其是下面這種場(chǎng)景:

//在一個(gè)上層剃浇,比如Controller中
@GetMapping("/coupon/disable/{id}")      //失效某張優(yōu)惠券巾兆,偷懶就不用Post了
public ActionResponse disableCoupon(@PathVariable("id") Long id){
      Coupon coupon = couponRepository.getById(id);
    
      //錯(cuò)誤,禁止;⑶簟=撬堋!
      coupon.setStatus(CouponStatus.DISABLED);
    
      //正確的應(yīng)該是
      coupon.disable();
}

public class Coupon{
    
  private CouponStatus status;
  
  public void disable(){
      status = CouponStatus.DISABLED;
  }
}

一定會(huì)有同學(xué)馬上提出疑問(wèn)

才一行代碼淘讥,為什么不能直接用set圃伶?強(qiáng)迫癥嗎?

不否認(rèn)蒲列,這個(gè)規(guī)范窒朋,的確有點(diǎn)強(qiáng)迫癥,但是真的是有好處的蝗岖。

領(lǐng)域設(shè)計(jì)的思想里侥猩,嚴(yán)格意義上來(lái)說(shuō),Get和Set都是不能隨便暴露的抵赢,尤其是Set欺劳,是在修改這個(gè)系統(tǒng),是有一定風(fēng)險(xiǎn)與危害的铅鲤,那么划提,任何一個(gè)set,都一定是有原因的,一定是要歸屬到一個(gè)具體的業(yè)務(wù)命令操作中的邢享。

其實(shí)鹏往,我在思考這套規(guī)范期間,一度將set方法直接設(shè)置成本包可見的級(jí)別骇塘,希望通過(guò)Java編譯報(bào)錯(cuò)來(lái)杜絕這種情況掸犬,但是這樣又和上面提到的service的模式出現(xiàn)了沖突袜漩,最終只能作罷。

工廠

到此為止湾碎,我們可以認(rèn)可,所有的命令操作奠货,都將會(huì)歸類到Entity或者Service中

但有一個(gè)特例介褥,這里有必要提出來(lái)單獨(dú)說(shuō)一下:一個(gè)實(shí)體的創(chuàng)建,也就是增刪改查中的 增 的操作

因?yàn)?strong>刪递惋,改的操作柔滔,都是先找到一個(gè)實(shí)體,然后進(jìn)行操作萍虽。但創(chuàng)建卻不同睛廊,因?yàn)樵趫?zhí)行創(chuàng)建操作之前,這個(gè)實(shí)體都是不存在的杉编,你怎么找超全?就更加不可能有類似 order.create(params)這種代碼出現(xiàn)了,order尚且不存于世呢邓馒!
所以嘶朱,這里,自然的想到通過(guò)創(chuàng)建OrderCreateService來(lái)處理光酣,但考慮到新增的特殊性疏遏,建議直接用工廠模式來(lái)做,即OrderFactory

對(duì)于OrderFactory救军,它的責(zé)任并非簡(jiǎn)簡(jiǎn)單單new Order()然后一堆setXX后完事财异。詳細(xì)說(shuō)明如下:

  1. 負(fù)責(zé)創(chuàng)建聚合根對(duì)象,比如訂單聚合中的Order唱遭,往往創(chuàng)建會(huì)有諸多不同的場(chǎng)景戳寸,比如創(chuàng)建一個(gè)空對(duì)象,或者創(chuàng)建有很多默認(rèn)組件的對(duì)象等等胆萧,這個(gè)就根據(jù)業(yè)務(wù)場(chǎng)景來(lái)了庆揩。總之返回值一定是一個(gè)新創(chuàng)建的實(shí)體類跌穗。
  2. 負(fù)責(zé)創(chuàng)建聚合中其他對(duì)象订晌,但是這種場(chǎng)景仔細(xì)想來(lái),并不會(huì)太多蚌吸。因?yàn)榫酆现衅渌案綄賹?shí)體”的創(chuàng)建往往會(huì)以聚合根實(shí)體的某一個(gè)命令操作相關(guān)锈拨。比如訂單變更記錄OrderTrack的創(chuàng)建往往是伴隨Order的各種各樣的操作,比如訂單創(chuàng)建羹唠,支付奕枢,發(fā)貨娄昆,取消等等,而大部分時(shí)候不需要單獨(dú)出現(xiàn)諸如OrderFactory.createOrderTrack的情況缝彬。
  3. Factory中原則上是允許觸發(fā)對(duì)其他領(lǐng)域聚合的數(shù)據(jù)變更的萌焰,因?yàn)樗且粋€(gè)特殊的領(lǐng)域Service。但從一般的業(yè)務(wù)場(chǎng)景來(lái)說(shuō)谷浅,這種情況并不多見扒俯,因?yàn)閯?chuàng)建后立即變動(dòng)某個(gè)其他領(lǐng)域的數(shù)據(jù),往往會(huì)直接在應(yīng)用層加入代碼一疯,或者通過(guò)事件來(lái)處理(事件后文會(huì)介紹)撼玄。

事件

領(lǐng)域事件,在最早的《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書中墩邀,并未提及掌猛。在之后的相關(guān)書籍,諸如《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》中眉睹,有將其作為一等公民的身份進(jìn)行詳細(xì)講解荔茬。很慚愧,這一塊我一直沒有GET到其精髓辣往,所以我只是結(jié)合更廣義上的事件兔院,來(lái)做了一些分析,如有不合適的地方站削,也歡迎各位拍磚坊萝。

說(shuō)到事件,大家一定能聯(lián)想到事件的廣播许起,事件的處理十偶。沒錯(cuò),事件是一個(gè)非常好的解耦和工具园细,也是一個(gè)非常舒服的“梳理工具”惦积,因?yàn)樵谟懻撔枨蟮臅r(shí)候,經(jīng)常能夠看到非趁推担“順理成章”的事件場(chǎng)景描述:

當(dāng)用戶創(chuàng)建了一個(gè)訂單后狮崩,要同時(shí)生成一個(gè)訂單變更記錄 ------ (1)
當(dāng)用戶的訂單支付成功后,要同時(shí)為這個(gè)用戶生成一個(gè)XX獎(jiǎng)勵(lì)券鹿寻,并且用戶活躍積分+10   ------ (2)

這一些睦柴,都太符合“事件”了,這些有些是在項(xiàng)目第一版的時(shí)候就清楚了毡熏,有些則隨著版本迭代不斷加入坦敌。
但是回過(guò)頭的仔細(xì)想想,如果遇到這種需求場(chǎng)景的時(shí)候,大家在實(shí)際的開發(fā)中狱窘,都真的用了事件嗎杜顺?不論是單機(jī)應(yīng)用還是分布式應(yīng)用,我們?cè)诓患尤胧录C(jī)制的前提下蘸炸,上面這些功能通過(guò)直接“調(diào)接口”都是完全能滿足的躬络。

沒錯(cuò),所以對(duì)于事件模式搭儒,我們可以倡導(dǎo)一個(gè)原則:所有的事件洗鸵,從重構(gòu)中得來(lái)
從重構(gòu)中得來(lái),意味著仗嗦,我們沒必要在需求一開始就大量采用事件的做法,即使需求描述中有“當(dāng)/如果...就...”甘凭,因?yàn)榻^大部分時(shí)候稀拐,我們往往無(wú)法得知之后的產(chǎn)品的發(fā)展方向是什么,過(guò)早的事件設(shè)計(jì)(尤其是分布式系統(tǒng))會(huì)給代碼的閱讀流暢性丹弱,事務(wù)管理等等帶來(lái)更大難度德撬。

事件的優(yōu)勢(shì)在于“一處廣播,多處接收”躲胳,所以蜓洪,當(dāng)“接收方”越來(lái)越多的時(shí)候,也是事件機(jī)制的優(yōu)勢(shì)能體先出來(lái)地時(shí)候坯苹。所以隆檀,我認(rèn)為最佳的實(shí)踐方式,或者更容易推廣的實(shí)踐方式粹湃,還是跟著版本迭代來(lái)不斷優(yōu)化代碼恐仑,在逐漸清晰地產(chǎn)品發(fā)展方向和擴(kuò)展方向上,將原有的“直接調(diào)用”轉(zhuǎn)變成“事件處理”为鳄。一般來(lái)說(shuō)裳仆,當(dāng)“接收方”出現(xiàn)2-3個(gè)的時(shí)候,可以開始考慮轉(zhuǎn)變成事件機(jī)制了孤钦,比如上面的(2)

當(dāng)然歧斟,這里并不否認(rèn)在第一時(shí)間就加入事件機(jī)制的做法,只是建議如果確定要在一開始就這樣做偏形,希望這種做法的開發(fā)負(fù)責(zé)人務(wù)必對(duì)業(yè)務(wù)的擴(kuò)展方向有足夠清楚的認(rèn)識(shí)與了解静袖。(比如一家公司在原有系統(tǒng)上開發(fā)新的升級(jí)版系統(tǒng),這個(gè)時(shí)候壳猜,可以在第一時(shí)間就做好設(shè)計(jì)優(yōu)化勾徽,因?yàn)橛泻芎玫臉I(yè)務(wù)背景基礎(chǔ))

至于具體的代碼實(shí)現(xiàn)方式,在單機(jī)應(yīng)用中,Spring有很好的事件機(jī)制喘帚,而且能夠支持事務(wù)的完整性畅姊。而分布式系統(tǒng)中,更多的接用消息中間件來(lái)實(shí)現(xiàn)吹由。

下一篇 關(guān)于重構(gòu)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末若未,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子倾鲫,更是在濱河造成了極大的恐慌粗合,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乌昔,死亡現(xiàn)場(chǎng)離奇詭異隙疚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)磕道,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門供屉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人溺蕉,你說(shuō)我怎么就攤上這事伶丐。” “怎么了疯特?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵哗魂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我漓雅,道長(zhǎng)录别,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任故硅,我火速辦了婚禮庶灿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吃衅。我一直安慰自己往踢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布徘层。 她就那樣靜靜地躺著峻呕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪趣效。 梳的紋絲不亂的頭發(fā)上瘦癌,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音跷敬,去河邊找鬼讯私。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斤寇。 我是一名探鬼主播桶癣,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼娘锁!你這毒婦竟也來(lái)了牙寞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤莫秆,失蹤者是張志新(化名)和其女友劉穎间雀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镊屎,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惹挟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缝驳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匪煌。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖党巾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情霜医,我是刑警寧澤齿拂,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站肴敛,受9級(jí)特大地震影響署海,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜医男,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一砸狞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镀梭,春花似錦刀森、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至透罢,卻和暖如春榜晦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羽圃。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工乾胶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓识窿,卻偏偏與公主長(zhǎng)得像斩郎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腕扶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344