掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
加入阿里健康之后,我所在的團(tuán)隊也在積極推進(jìn)領(lǐng)域驅(qū)動設(shè)計的應(yīng)用,相關(guān)同學(xué)也曾給出優(yōu)秀的腳手架代碼,但目前看起來落地情況并不太理想,個人淺見,造成這種結(jié)果主要有四個原因。

成都創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供黃龍網(wǎng)站建設(shè)、黃龍做網(wǎng)站、黃龍網(wǎng)站設(shè)計、黃龍網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、黃龍企業(yè)網(wǎng)站模板建站服務(wù),十載黃龍做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。
筆者曾在研發(fā)過程中研究、實踐過領(lǐng)域驅(qū)動編程,對領(lǐng)域驅(qū)動框架 Axon Framework 也做了深入的了解,(也許是因為業(yè)務(wù)場景相對簡單)當(dāng)時落地效果還不錯。拋卻架構(gòu)師的視角,從一線研發(fā)同學(xué)的角度來看,基于領(lǐng)域驅(qū)動編程的核心優(yōu)勢在于:
領(lǐng)域驅(qū)動開發(fā)最重要的當(dāng)然是正確地進(jìn)行領(lǐng)域拆解,這個拆解工作可以在理論的指導(dǎo)下,結(jié)合設(shè)計者對業(yè)務(wù)的深入分析和充分理解進(jìn)行。本文假定開發(fā)前已經(jīng)進(jìn)行了領(lǐng)域劃分,側(cè)重于研究編碼階段具體如何實踐才能體現(xiàn)領(lǐng)域驅(qū)動的優(yōu)勢。
以保險業(yè)務(wù)為例來進(jìn)行編程實踐,一個高度抽象的保險領(lǐng)域劃分如圖所示。通過用例分析,我們把整個業(yè)務(wù)劃分成產(chǎn)品域、承保、核保、理賠等多個領(lǐng)域(Bounded-Context),每個領(lǐng)域又可以根據(jù)業(yè)務(wù)發(fā)展情況拆分子域。當(dāng)然,完備保險業(yè)務(wù)要比圖中展現(xiàn)的復(fù)雜太多,這里我們不作為業(yè)務(wù)知識介紹的篇章,只是為了方便后續(xù)的代碼實踐。
可以使用不同的 Java 項目發(fā)布不同的微服務(wù)對領(lǐng)域進(jìn)行隔離,也可以在同一個 Java 項目中,使用不同 module 進(jìn)行領(lǐng)域隔離。這里我們使用 module 進(jìn)行領(lǐng)域隔離的實現(xiàn)。但是無論采用何種方式進(jìn)行領(lǐng)域隔離,領(lǐng)域之間的交互只能使用對方的二方包或者 API 層提供的 HTTP 服務(wù),而不能直接引入其他領(lǐng)域的其他服務(wù)。
在每個領(lǐng)域內(nèi)部,相對于 MVC 對應(yīng)用三層架構(gòu)的拆分,領(lǐng)域驅(qū)動的設(shè)計將應(yīng)用模塊內(nèi)部分為如圖示的四層。
負(fù)責(zé)直接面向外部用戶或者系統(tǒng),接收外部輸入,并返回結(jié)果,例如二方包的實現(xiàn)類、Spring MVC 中的 Controller、特定的數(shù)據(jù)視圖轉(zhuǎn)換器等通常位于該層。在代碼層面常常使用的包命名可以是 interface, api, facade 等。用戶接口層的入?yún)?、出參類定義采用 POJO 風(fēng)格。
用戶接口層是輕的一層,不含業(yè)務(wù)邏輯。安全認(rèn)證,簡單的入?yún)⑿r?例如使用 @Valid 注解),訪問日志記錄,統(tǒng)一的異常處理邏輯,統(tǒng)一返回值封裝應(yīng)當(dāng)在這層完成。
用戶接口層所需要的功能實現(xiàn)是由應(yīng)用層完成,這里一般不需要進(jìn)行依賴倒置。編碼時,該層可以直接引入應(yīng)用層中定義的接口,因而該層依賴應(yīng)用層。需要注意的是,雖然理論上用戶接口層可以直接使用領(lǐng)域?qū)雍突A(chǔ)設(shè)施層的能力,但這里建議大家在對這種用法熟練掌握前,最好采用嚴(yán)格的分層架構(gòu),即當(dāng)前層只依賴其下方相鄰的一層。
應(yīng)用層具體實現(xiàn)接口層中需要功能,但該層并不實現(xiàn)真正的業(yè)務(wù)規(guī)則,而是根據(jù)實際的 use case 來協(xié)調(diào)調(diào)用領(lǐng)域?qū)犹峁┑哪芰Α?/p>
消息發(fā)送、事件監(jiān)聽、事務(wù)控制等建議在這一層實現(xiàn)。在代碼層面常常使用的包命名可以是 application, service, manager 等。它用來取代 Spring MVC 中 service 層,并把業(yè)務(wù)邏輯轉(zhuǎn)移到領(lǐng)域?qū)印?/p>
領(lǐng)域?qū)用嫦驅(qū)ο蟮?,它主要用來體現(xiàn)和實現(xiàn)領(lǐng)域里的對象所具備的固有能力。因此,在領(lǐng)域驅(qū)動編程中,領(lǐng)域?qū)拥木幊虒崿F(xiàn)是不允許依賴其他外部對象的,領(lǐng)域?qū)拥木幊淌窃谖覀儗︻I(lǐng)域內(nèi)的對象所具備的固有能力和它要在當(dāng)前業(yè)務(wù)場景下展現(xiàn)什么樣的能力有一定了解后,可以直接編碼實現(xiàn)的。
例如我們最開始接觸面向?qū)ο蟮木幊痰臅r候,常常會遇到的一個例子是鳥會飛、狗會游泳,假設(shè)我們的業(yè)務(wù)域只關(guān)心這些對象的運動,我們可以做如下的實現(xiàn)。
public interface Moveable {
void move();
}
public abstract class Animal implements Moveable {}
public class Bird extends Animal {
public void move(){
//try to fly
System.out.println("I'am flying");
}
}
public class Dog extends Animal {
public void move(){
//try to swim
System.out.println("I'am swimming");
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.
基于領(lǐng)域驅(qū)動的編程需要這樣(充血模型)去實現(xiàn)對象的能力,而不是像我們在 MVC 架構(gòu)中常常使用貧血模型,把業(yè)務(wù)邏輯寫在 service 中。
當(dāng)然,即使采用了這樣的編程方式,距離實現(xiàn)領(lǐng)域驅(qū)動還差的遠(yuǎn),一些看似簡單的問題就可能給我們帶來巨大的不安感。例如復(fù)雜的對象應(yīng)當(dāng)如何初始化和持久化?同樣一個事物在不同領(lǐng)域都存在,但其關(guān)注點不同時這個事物應(yīng)當(dāng)分別怎么抽象?不同領(lǐng)域的對象需要對方的信息時,應(yīng)當(dāng)怎么獲取?
這些問題,我們也會在代碼示例部分嘗試給出一些參考的方案。
基礎(chǔ)設(shè)施層為上面各層提供通用的技術(shù)能力,例如監(jiān)聽、發(fā)送消息的能力,數(shù)據(jù)庫/緩存/NoSQL數(shù)據(jù)庫/文件系統(tǒng)等倉儲的 CRUD 能力等。
根據(jù)對領(lǐng)域驅(qū)動設(shè)計各層的進(jìn)一步分析,一個更加具體化的分層結(jié)構(gòu)如下。
基于上面的分層原則,前述保險領(lǐng)域一個可以參考的代碼結(jié)構(gòu)如下,我們將在下面編碼示例詳細(xì)講解每一個分包的理念和作用。
理論上,DOMAIN 不依賴其他層次且是業(yè)務(wù)核心,我們應(yīng)當(dāng)先編寫領(lǐng)域?qū)哟a,但是一則由于我們對保險領(lǐng)域知識的欠缺,可能不清楚保單到底有哪些固有能力;二則為了便于講解,因此我們直接借助一個用例來展示代碼。
這里用例 1 是用例 2 的前置用例,我們假定用例 1 已經(jīng)順利完成(用例 1 中完成了費率計算),只來實現(xiàn)用例 2,并且用例 2 也只是大略的實現(xiàn),只要能把代碼樣式展示即可。
其中 client 是對 inusurance-client (公共二方包) 部分的實現(xiàn),web 是 rest 風(fēng)格接口的實現(xiàn)。
@AllArgsConstructor
@RestController
@RequestMapping("/insure")
public class PolicyController {
private final InsuranceInsureService insuranceInsureService;
/**
* 投保出單
* @param request
* @return 保單 ID
*/
@RequestMapping(value = "/issue-policy", method = RequestMethod.POST)
public String issuePolicy(IssuePolicyRequest request){
return insuranceInsureService.issuePolicy(request);
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
這里用到的入?yún)⒑头祷刂档念惗荚趹?yīng)用層中定義。
注意,在領(lǐng)域編程實踐中,會需要非常多的類型轉(zhuǎn)換,我們可以借助一些框架(例如 MapStruct[2])來減少這些類型轉(zhuǎn)換給我們帶來的繁瑣工作。
@Service
@AllArgsConstructor
public class InsuranceInsureServiceImpl implements InsuranceInsureService {
private final PolicyFactory policyFactory;
private final StakeHolderConvertor stakeHolderConvertor;
private final PolicyService policyService;
/**
* 事務(wù)控制一般在應(yīng)用層
* 但是需要注意底層存儲對事務(wù)的支持特性
* 底層是分庫分表時,可能需要其他手段來保證事務(wù),或者將非核心的操作從事務(wù)中剝離(例如數(shù)據(jù)庫 ID 生成)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String issuePolicy(IssuePolicyRequest request) {
Policy policy = policyFactory.createPolicy(request.getProductId(),
stakeHolderConvertor.convert(request.getStakeHolders()));
//出單流程控制
policyService.issue(policy);
PolicyIssuedMessage message = new PolicyIssuedMessage();
message.setPolicyId(policy.getId());
MQPublisher.publish(MQConstants.INSURANCE_TOPIC, MQConstants.POLICY_ISSUED_TAG, message);
return policy.getId().toString();
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.
這里代碼展示的是應(yīng)用層對用例 2 的處理:
這里領(lǐng)域?qū)右还灿形鍌€一級分包:
因此這里工廠的核心作用是從各處拉取初始化聚合或?qū)嶓w所需要的外部數(shù)據(jù):
@Service
@AllArgsConstructor
public class PolicyFactory {
/**
* 產(chǎn)品領(lǐng)域防腐層服務(wù)
*/
private final ProductService productService;
/**
* 從各種數(shù)據(jù)來源查詢直接能查到的前置數(shù)據(jù),填充到 policy 中
* @param productId
* @param stakeHolders
* @return
*/
public Policy createPolicy(Long productId, ListstakeHolders) {
PolicyProduct product = productService.getById(productId);
//其他填充數(shù)據(jù),這里調(diào)用了聚合自身的靜態(tài)工廠方法
Policy policy = Policy.create(product, stakeHolders);
return policy;
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
按照領(lǐng)域驅(qū)動設(shè)計的最佳實踐,領(lǐng)域?qū)ο竽P椭胁辉试S出現(xiàn) service、repository 這些用以獲取外部信息的東西,它的核心概念是一個完備的實體初始化完成后,它能做什么,或者它經(jīng)歷了什么之后狀態(tài)會發(fā)生怎樣的變化。
下面是領(lǐng)域內(nèi)核心的聚合 Policy 的示例代碼:
@Getter
public class Policy {
private Long id;
private PolicyProduct product;
private ListstakeHolders;
private Date issueTime;
/**
* 工廠方法
* @param product
* @param stakeHolders
* @return
*/
public static Policy create(PolicyProduct product, ListstakeHolders){
Policy policy = new Policy();
policy.product = product;
policy.stakeHolders = stakeHolders;
return policy;
}
/**
* 保單出單
*/
public void issue(Long id) {
this.id = id;
this.issueTime = new Date();
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.
@Service
@AllArgsConstructor
public class PolicyService {
private final InsureUnderwriteService insureUnderwriteService;
private final PolicyRepository policyRepository;
public void issue(Policy policy) {
if(!insureUnderwriteService.underwrite(policy)){
throw new BizException("核保失敗");
}
policy.issue(IdGenerator.generate());
//保存信息
//policyRepository.save(policy);
policyRepository.create(policy);
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
這里注意我們注掉了一行 policyRepository.save(policy);,那么為什么要區(qū)別 save 和 create 呢?
save 是領(lǐng)域驅(qū)動設(shè)計中最正確的做法:我的聚合或者實體有變動,倉儲不用關(guān)心是新建還是更新,幫我保存起來就好了。聽上去很美好,但對關(guān)系型數(shù)據(jù)庫存儲卻是很不友好的。因此,在我們的場景里,需要違背一下書上所謂的最佳實踐,我們告訴倉儲是要新建還是更新,甚至如果是更新的話更新的是哪些列。
另外領(lǐng)域驅(qū)動的最佳實踐是基于事件驅(qū)動的,AxonFramework 對其有完美的實現(xiàn),應(yīng)用層發(fā)出一個 IssuePolicyCommand 指令,領(lǐng)域?qū)咏邮赵撝噶睿瓿杀蝿?chuàng)建后發(fā)出PolicyIssuedEvent,該 event 會被監(jiān)聽并且持久化到 event store 中。這種方式目前看起來在我們這里落地的可能性不大,不做更多介紹。
這里只展示了 repository 的實現(xiàn),但實際上這里還有 RPC 調(diào)用的二方包實現(xiàn)類注入等很多內(nèi)容。上文說到領(lǐng)域?qū)硬魂P(guān)心倉儲的實現(xiàn),交由基礎(chǔ)設(shè)施層負(fù)責(zé)?;A(chǔ)設(shè)施層可以根據(jù)需要使用關(guān)系型數(shù)據(jù)庫、緩存或者NoSQL,領(lǐng)域?qū)邮菬o感知的。這里我們以關(guān)系型數(shù)據(jù)庫為例來,dao 和 dataobject 等都可以使用例如 mybatis generator 等工具生成,領(lǐng)域?qū)ο?和 dataobject 之間的轉(zhuǎn)換由 convertor 負(fù)責(zé)。
@Repository
@AllArgsConstructor
public class PolicyRepositoryImpl implements PolicyRepository {
private final PolicyDAO policyDAO;
private final StakeHolderDAO stakeHolderDAO;
private final PolicyConvertor policyConvertor;
private final StakeHolderConvertor stakeHolderConvertor;
@Override
public String save(Policy policy) {
throw new UnsupportedOperationException();
}
@Override
public String create(Policy policy) {
policyDAO.insert(policyConvertor.convert(policy));
stakeHolderDAO.insertBatch(stakeHolderConvertor.convert(policy));
//...其它數(shù)據(jù)入庫
return policy.getId().toString();
}
@Override
public void updatePolicyStatus(String newStatus) {
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.
這部分代碼比較簡單,無需贅言。
關(guān)于領(lǐng)域驅(qū)動,筆者仍處于初學(xué)者階段,再好的設(shè)計,隨著業(yè)務(wù)的發(fā)展,代碼也難免變得混亂,這個過程中,每個參與者都有責(zé)任。最后,總結(jié)一下我們維持代碼初心的一些原則,和大家分享。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流