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è)行為熙参,需要做的事情有:
- 訂單狀態(tài)標(biāo)記為取消
- 訂單變更記錄插入一條,“訂單取消”
根據(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)作:
- 訂單狀態(tài)改為支付中
- 商品庫(kù)存對(duì)應(yīng)扣減
- 用戶若使用了優(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ī)矩,深入探討一下:
- 類被命名為訂單支付服務(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ù)用 -
service
裕偿,我們可以給與其足夠的權(quán)限,只要它需要,它可以無(wú)所顧忌地獲取所有的上下文組件萎馅,不管是jdbc組件,還是外部rpc組件命咐,都是可以的壹堰。因?yàn)榻o它的定義,本來(lái)就是多聚合的事務(wù)處理類爬骤,所以充石,只要它能保證事務(wù)的安全性,保證業(yè)務(wù)的完整盖腕,這一切都是沒問(wèn)題的(這里暫時(shí)不討論分布式事務(wù)問(wèn)題赫冬,那是另外要一個(gè)議題) - 庫(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ō)明如下:
- 負(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í)體類跌穗。 - 負(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
的情況缝彬。 -
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)吹由。