新聞中心
一、前言
加入阿里健康之后,我所在的團隊也在積極推進領(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ù)。
- 大家更熟悉 MVC 的編程模式,需要快速實現(xiàn)某個功能的時候,往往傾向于使用較為穩(wěn)妥、熟悉的方式。
- 大家對領(lǐng)域驅(qū)動編程應(yīng)該怎么編寫并沒有一個統(tǒng)一的認(rèn)知(Axon Framework[1] 對領(lǐng)域驅(qū)動設(shè)計實現(xiàn)的非常好,但它太“重”了)。
- DDD 落地本身就比較難,往往需要事件驅(qū)動和 Event Store 來完美實現(xiàn),而這二者是我們不常用的。
- 領(lǐng)域驅(qū)動設(shè)計是面向復(fù)雜系統(tǒng)的,業(yè)務(wù)發(fā)展初期看上去都比較簡單,一上來就搞領(lǐng)域驅(qū)動設(shè)計有過度設(shè)計之嫌。這也是領(lǐng)域驅(qū)動設(shè)計常常在系統(tǒng)不得不重構(gòu)的是時候才被拿出來討論的原因。
筆者曾在研發(fā)過程中研究、實踐過領(lǐng)域驅(qū)動編程,對領(lǐng)域驅(qū)動框架 Axon Framework 也做了深入的了解,(也許是因為業(yè)務(wù)場景相對簡單)當(dāng)時落地效果還不錯。拋卻架構(gòu)師的視角,從一線研發(fā)同學(xué)的角度來看,基于領(lǐng)域驅(qū)動編程的核心優(yōu)勢在于:
- 實施面向?qū)ο蟮木幊棠J剑M而實現(xiàn)高內(nèi)聚、低耦合。
- 在復(fù)雜業(yè)務(wù)系統(tǒng)的迭代過程中,保證代碼結(jié)構(gòu)不會無限制地變得混亂,因此保證系統(tǒng)可持續(xù)維護。
領(lǐng)域驅(qū)動開發(fā)最重要的當(dāng)然是正確地進行領(lǐng)域拆解,這個拆解工作可以在理論的指導(dǎo)下,結(jié)合設(shè)計者對業(yè)務(wù)的深入分析和充分理解進行。本文假定開發(fā)前已經(jīng)進行了領(lǐng)域劃分,側(cè)重于研究編碼階段具體如何實踐才能體現(xiàn)領(lǐng)域驅(qū)動的優(yōu)勢。
二、保險領(lǐng)域知識簡介
以保險業(yè)務(wù)為例來進行編程實踐,一個高度抽象的保險領(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ù)的代碼實踐。
三、領(lǐng)域驅(qū)動開發(fā)的代碼結(jié)構(gòu)
1. 領(lǐng)域驅(qū)動的代碼分層
可以使用不同的 Java 項目發(fā)布不同的微服務(wù)對領(lǐng)域進行隔離,也可以在同一個 Java 項目中,使用不同 module 進行領(lǐng)域隔離。這里我們使用 module 進行領(lǐng)域隔離的實現(xià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)部分為如圖示的四層。
(1) 用戶接口層
負(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)用層完成,這里一般不需要進行依賴倒置。編碼時,該層可以直接引入應(yīng)用層中定義的接口,因而該層依賴應(yīng)用層。需要注意的是,雖然理論上用戶接口層可以直接使用領(lǐng)域?qū)雍突A(chǔ)設(shè)施層的能力,但這里建議大家在對這種用法熟練掌握前,最好采用嚴(yán)格的分層架構(gòu),即當(dāng)前層只依賴其下方相鄰的一層。
(2) 應(yī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>
(3) 領(lǐng)域?qū)?/h4>
領(lǐng)域?qū)用嫦驅(qū)ο蟮模饕脕眢w現(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)怎么獲取?
這些問題,我們也會在代碼示例部分嘗試給出一些參考的方案。
(4) 基礎(chǔ)設(shè)施層
基礎(chǔ)設(shè)施層為上面各層提供通用的技術(shù)能力,例如監(jiān)聽、發(fā)送消息的能力,數(shù)據(jù)庫/緩存/NoSQL數(shù)據(jù)庫/文件系統(tǒng)等倉儲的 CRUD 能力等。
2. 小結(jié)
根據(jù)對領(lǐng)域驅(qū)動設(shè)計各層的進一步分析,一個更加具體化的分層結(jié)構(gòu)如下。
基于上面的分層原則,前述保險領(lǐng)域一個可以參考的代碼結(jié)構(gòu)如下,我們將在下面編碼示例詳細(xì)講解每一個分包的理念和作用。
四、領(lǐng)域驅(qū)動開發(fā)的代碼
理論上,DOMAIN 不依賴其他層次且是業(yè)務(wù)核心,我們應(yīng)當(dāng)先編寫領(lǐng)域?qū)哟a,但是一則由于我們對保險領(lǐng)域知識的欠缺,可能不清楚保單到底有哪些固有能力;二則為了便于講解,因此我們直接借助一個用例來展示代碼。
1. 用例
- 用戶在前端頁面選擇保險產(chǎn)品,選擇可選的保障責(zé)任,輸入投/被保人信息,選擇支付方式(分期/躉交等)并支付后提交投保請求;
- 服務(wù)端接受投保請求 -> 核保 -> 出單 -> 下發(fā)保單權(quán)益。
這里用例 1 是用例 2 的前置用例,我們假定用例 1 已經(jīng)順利完成(用例 1 中完成了費率計算),只來實現(xiàn)用例 2,并且用例 2 也只是大略的實現(xiàn),只要能把代碼樣式展示即可。
2. 用戶接口層編程實踐
(1) 分包結(jié)構(gòu)
其中 client 是對 inusurance-client (公共二方包) 部分的實現(xiàn),web 是 rest 風(fēng)格接口的實現(xiàn)。
(2) 用例代碼
@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)用層中定義。
3. 應(yīng)用層編程實踐
(1) 分包結(jié)構(gòu)
- 其中最外層接口是面向具體業(yè)務(wù)場景的,可以根據(jù)業(yè)務(wù)發(fā)展再進行分包。
- pojo 包中定義了應(yīng)用層用到的各種數(shù)據(jù)類(上面的 IssuePolicyRequest 就在這里)及其向其他層傳播時需要進行類型轉(zhuǎn)換的轉(zhuǎn)化器。
- tasks 包中定義了一些定時任務(wù)的入口。
注意,在領(lǐng)域編程實踐中,會需要非常多的類型轉(zhuǎn)換,我們可以借助一些框架(例如 MapStruct[2])來減少這些類型轉(zhuǎn)換給我們帶來的繁瑣工作。
(2) 用例代碼:
@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ū)拥墓S類構(gòu)建 Policy 聚合。如果需要傳遞復(fù)雜對象,需要先用類型轉(zhuǎn)換器將應(yīng)用層的數(shù)據(jù)類轉(zhuǎn)化為領(lǐng)域?qū)拥膶嶓w類或者值對象。
- 使用領(lǐng)域?qū)臃?wù)控制出單流程
- 發(fā)送出單成功消息,其他領(lǐng)域監(jiān)聽到感興趣的消息會進行響應(yīng)。
4. 領(lǐng)域?qū)泳幊虒嵺`
(1) 分包結(jié)構(gòu)
這里領(lǐng)域?qū)右还灿形鍌€一級分包:
- anticorruption 是領(lǐng)域防腐層,是當(dāng)前領(lǐng)域需要獲知其他領(lǐng)域或者外部信息時,對其他領(lǐng)域二方包的封裝。防腐層從代碼層面來看,可以避免調(diào)用外部客戶端時,在領(lǐng)域內(nèi)部進行復(fù)雜的參數(shù)拼裝和結(jié)果的轉(zhuǎn)換。
- factory 解決了復(fù)雜聚合的初始化問題。我們設(shè)計好領(lǐng)域模型供外部調(diào)用,但如果外部也必須使用如何裝配這個對象,則必須知道對象的內(nèi)部結(jié)構(gòu)。對調(diào)用方開發(fā)來說這是很不友好的。其次,復(fù)雜對象或者聚合當(dāng)中的領(lǐng)域知識(業(yè)務(wù)規(guī)則)需要得到滿足,如果讓外部自己裝配復(fù)雜對象或聚合的話,就會將領(lǐng)域知識泄露到調(diào)用方代碼中去。需要注意的是,這里主要是把聚合或?qū)嶓w需要的數(shù)據(jù)填充進來,而不涉及對象的行為。
因此這里工廠的核心作用是從各處拉取初始化聚合或?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.
- model 中是領(lǐng)域?qū)ο蟮亩x。其中 vo 包中定義了領(lǐng)域內(nèi)用到的值對象??梢钥吹竭@里有PolicyProduct 這樣一個保險產(chǎn)品類,在投保領(lǐng)域,我們關(guān)注的是和保單相關(guān)的某個產(chǎn)品及其快照信息,因此我們在這里定義一個保單保險產(chǎn)品類,防腐層負(fù)責(zé)把從產(chǎn)品域獲得的保險產(chǎn)品信息轉(zhuǎn)換為我們關(guān)心的保單保險產(chǎn)品類對象。
按照領(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.
- repository 是倉儲包,只定義倉儲接口,不關(guān)心具體實現(xiàn),具體的實現(xiàn)交由基礎(chǔ)設(shè)施層負(fù)責(zé),體現(xiàn)了依賴倒置的思想。
- service 是領(lǐng)域服務(wù),它定義一些不屬于領(lǐng)域?qū)ο蟮男袨?,但是又有必要的操作,比如一些流程控制?/li>
(2) 用例代碼:
@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 中。這種方式目前看起來在我們這里落地的可能性不大,不做更多介紹。
5. 基礎(chǔ)設(shè)施層編程實踐
(1) 分包結(jié)構(gòu)
這里只展示了 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é)。
(2) 用例代碼
@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.
這部分代碼比較簡單,無需贅言。
五、結(jié)語
關(guān)于領(lǐng)域驅(qū)動,筆者仍處于初學(xué)者階段,再好的設(shè)計,隨著業(yè)務(wù)的發(fā)展,代碼也難免變得混亂,這個過程中,每個參與者都有責(zé)任。最后,總結(jié)一下我們維持代碼初心的一些原則,和大家分享。
- 深入理解業(yè)務(wù)場景,分析用例,進行正確的領(lǐng)域劃分。
- 確定好實現(xiàn)方式后,大家盡量按照既定模式/風(fēng)格編程,有異議的地方可以一起討論后統(tǒng)一改動。
- 不引入不必要的復(fù)雜度。
- 不斷對系統(tǒng)設(shè)計進行優(yōu)化改進,對繁瑣的代碼,用設(shè)計模式進行優(yōu)化。
- 寫注釋。
名稱欄目:領(lǐng)域驅(qū)動編程,代碼怎么寫?
標(biāo)題路徑:http://m.5511xx.com/article/cdiisog.html


咨詢
建站咨詢
