共计 7895 个字符,预计需要花费 20 分钟才能阅读完成。
简介:相较于大家纯熟应用的 MVC 分层架构,畛域驱动设计更实用于简单业务零碎和须要继续迭代的软件系统的架构模型。对于畛域驱动设计的概念及劣势,能够参考的文献十分多,大多数的同学都看过相干的书籍,所以本文不探讨畛域驱动概念层面的货色,而是试图从编程实际的层面,对畛域驱动开发做一些简略的介绍。
作者 | 昌夜
起源 | 阿里技术公众号
一 前言
相较于大家纯熟应用的 MVC 分层架构,畛域驱动设计更实用于简单业务零碎和须要继续迭代的软件系统的架构模型。对于畛域驱动设计的概念及劣势,能够参考的文献十分多,大多数的同学都看过相干的书籍,所以本文不探讨畛域驱动概念层面的货色,而是试图从编程实际的层面,对畛域驱动开发做一些简略的介绍。
退出阿里衰弱之后,我所在的团队也在踊跃推动畛域驱动设计的利用,相干同学也曾给出优良的脚手架代码,但目前看起来落地状况并不太现实,集体浅见,造成这种后果次要有四个起因。
- 大家更相熟 MVC 的编程模式,须要疾速实现某个性能的时候,往往偏向于应用较为稳当、相熟的形式。
- 大家对畛域驱动编程应该怎么编写并没有一个对立的认知(Axon Framework[1] 对畛域驱动设计实现的十分好,但它太“重”了)。
- DDD 落地自身就比拟难,往往须要事件驱动和 Event Store 来完满实现,而这二者是咱们不罕用的。
- 畛域驱动设计是面向简单零碎的,业务倒退初期看上去都比较简单,一上来就搞畛域驱动设计有适度设计之嫌。这也是畛域驱动设计经常在零碎不得不重构的是时候才被拿进去探讨的起因。
笔者曾在研发过程中钻研、实际过畛域驱动编程,对畛域驱动框架 Axon Framework 也做了深刻的理解,(兴许是因为业务场景绝对简略)过后落地成果还不错。抛却架构师的视角,从一线研发同学的角度来看,基于畛域驱动编程的外围劣势在于:
- 施行面向对象的编程模式,进而实现高内聚、低耦合。
- 在简单业务零碎的迭代过程中,保障代码构造不会无限度地变得凌乱,因而保证系统可继续保护。
畛域驱动开发最重要的当然是正确地进行畛域拆解,这个拆解工作能够在实践的领导下,联合设计者对业务的深入分析和充沛了解进行。本文假设开发前曾经进行了畛域划分,侧重于钻研编码阶段具体如何实际能力体现畛域驱动的劣势。
二 保险畛域常识简介
以保险业务为例来进行编程实际,一个高度形象的保险畛域划分如图所示。通过用例剖析,咱们把整个业务划分成产品域、承保、核保、理赔等多个畛域(Bounded-Context),每个畛域又能够依据业务倒退状况拆分子域。当然,齐备保险业务要比图中展示的简单太多,这里咱们不作为业务知识介绍的篇章,只是为了不便后续的代码实际。
三 畛域驱动开发的代码构造
1 畛域驱动的代码分层
能够应用不同的 Java 我的项目公布不同的微服务对畛域进行隔离,也能够在同一个 Java 我的项目中,应用不同 module 进行畛域隔离。这里咱们应用 module 进行畛域隔离的实现。然而无论采纳何种形式进行畛域隔离,畛域之间的交互只能应用对方的二方包或者 API 层提供的 HTTP 服务,而不能间接引入其余畛域的其余服务。
在每个畛域外部,绝对于 MVC 对利用三层架构的拆分,畛域驱动的设计将利用模块外部分为如图示的四层。
用户接口层
负责间接面向内部用户或者零碎,接管内部输出,并返回后果,例如二方包的实现类、Spring MVC 中的 Controller、特定的数据视图转换器等通常位于该层。在代码层面经常应用的包命名能够是 interface, api, facade 等。用户接口层的入参、出参类定义采纳 POJO 格调。
用户接口层是轻的一层,不含业务逻辑。平安认证,简略的入参校验(例如应用 @Valid 注解),拜访日志记录,对立的异样解决逻辑,对立返回值封装该当在这层实现。
用户接口层所须要的性能实现是由应用层实现,这里个别不须要进行依赖倒置。编码时,该层能够间接引入应用层中定义的接口,因此该层依赖应用层。须要留神的是,尽管实践上用户接口层能够间接应用畛域层和基础设施层的能力,但这里倡议大家在对这种用法熟练掌握前,最好采纳严格的分层架构,即以后层只依赖其下方相邻的一层。
应用层
应用层具体实现接口层中须要性能,但该层并不实现真正的业务规定,而是依据理论的 use case 来协调调用畛域层提供的能力。
音讯发送、事件监听、事务管制等倡议在这一层实现。在代码层面经常应用的包命名能够是 application, service, manager 等。它用来取代 Spring MVC 中 service 层,并把业务逻辑转移到畛域层。
畛域层
畛域层面向对象的,它次要用来体现和实现畛域里的对象所具备的固有能力。因而,在畛域驱动编程中,畛域层的编程实现是不容许依赖其余内部对象的,畛域层的编程是在咱们对畛域内的对象所具备的固有能力和它要在以后业务场景下展示什么样的能力有肯定理解后,能够间接编码实现的。
例如咱们最开始接触面向对象的编程的时候,经常会遇到的一个例子是鸟会飞、狗会游泳,假如咱们的业务域只关怀这些对象的静止,咱们能够做如下的实现。
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");
}
}
基于畛域驱动的编程须要这样(充血模型)去实现对象的能力,而不是像咱们在 MVC 架构中经常应用贫血模型,把业务逻辑写在 service 中。
当然,即便采纳了这样的编程形式,间隔实现畛域驱动还差的远,一些看似简略的问题就可能给咱们带来微小的不安感。例如简单的对象该当如何初始化和长久化?同样一个事物在不同畛域都存在,但其关注点不同时这个事物该当别离怎么形象?不同畛域的对象须要对方的信息时,该当怎么获取?
这些问题,咱们也会在代码示例局部尝试给出一些参考的计划。
基础设施层
基础设施层为下面各层提供通用的技术能力,例如监听、发送音讯的能力,数据库 / 缓存 /NoSQL 数据库 / 文件系统等仓储的 CRUD 能力等。
2 小结
依据对畛域驱动设计各层的进一步剖析,一个更加具体化的分层构造如下。
基于下面的分层准则,前述保险畛域一个能够参考的代码构造如下,咱们将在上面编码示例具体解说每一个分包的理念和作用。
四 畛域驱动开发的代码
实践上,DOMAIN 不依赖其余档次且是业务外围,咱们该当先编写畛域层代码,然而一则因为咱们对保险畛域常识的欠缺,可能不分明保单到底有哪些固有能力;二则为了便于解说,因而咱们间接借助一个用例来展现代码。
1 用例
- 用户在前端页面抉择保险产品,抉择可选的保障责任,输出投 / 被保人信息,抉择领取形式(分期 / 趸交等)并领取后提交投保申请;
- 服务端承受投保申请 -> 核保 -> 出单 -> 下发保单权利。
这里用例 1 是用例 2 的前置用例,咱们假设用例 1 曾经顺利完成(用例 1 中实现了费率计算),只来实现用例 2,并且用例 2 也只是大略的实现,只有能把代码款式展现即可。
2 用户接口层编程实际
分包构造
其中 client 是对 inusurance-client (公共二方包) 局部的实现,web 是 rest 格调接口的实现。
用例代码
@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);
}
}
这里用到的入参和返回值的类都在应用层中定义。
3 应用层编程实际
1、分包构造
- 其中最外层接口是面向具体业务场景的,能够依据业务倒退再进行分包。
- pojo 包中定义了应用层用到的各种数据类(下面的 IssuePolicyRequest 就在这里)及其向其余层流传时须要进行类型转换的转化器。
- tasks 包中定义了一些定时工作的入口。
留神,在畛域编程实际中,会须要十分多的类型转换,咱们能够借助一些框架(例如 MapStruct[2])来缩小这些类型转换给咱们带来的繁琐工作。
2、用例代码
@Service
@AllArgsConstructor
public class InsuranceInsureServiceImpl implements InsuranceInsureService {
private final PolicyFactory policyFactory;
private final StakeHolderConvertor stakeHolderConvertor;
private final PolicyService policyService;
/**
* 事务管制个别在应用层
* 然而须要留神底层存储对事务的反对个性
* 底层是分库分表时,可能须要其余伎俩来保障事务,或者将非核心的操作从事务中剥离(例如数据库 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();
}
}
这里代码展现的是应用层对用例 2 的解决。
- 应用畛域层的工厂类构建 Policy 聚合。如果须要传递简单对象,须要先用类型转换器将应用层的数据类转化为畛域层的实体类或者值对象。
- 应用畛域层服务管制出单流程
- 发送出单胜利音讯,其余畛域监听到感兴趣的音讯会进行响应。
4 畛域层编程实际
1、分包构造
这里畛域层一共有 5 个一级分包。
- anticorruption 是畛域防腐层,是以后畛域须要获知其余畛域或者内部信息时,对其余畛域二方包的封装。防腐层从代码层面来看,能够防止调用内部客户端时,在畛域外部进行简单的参数拼装和后果的转换。
- factory 解决了简单聚合的初始化问题。咱们设计好畛域模型供内部调用,但如果内部也必须应用如何拆卸这个对象,则必须晓得对象的内部结构。对调用方开发来说这是很不敌对的。其次,简单对象或者聚合当中的畛域常识(业务规定)须要失去满足,如果让内部本人拆卸简单对象或聚合的话,就会将畛域常识泄露到调用方代码中去。须要留神的是,这里次要是把聚合或实体须要的数据填充进来,而不波及对象的行为。
因而这里工厂的核心作用是从各处拉取初始化聚合或实体所须要的内部数据。
@Service
@AllArgsConstructor
public class PolicyFactory {
/**
* 产品畛域防腐层服务
*/
private final ProductService productService;
/**
* 从各种数据起源查问间接能查到的前置数据,填充到 policy 中
* @param productId
* @param stakeHolders
* @return
*/
public Policy createPolicy(Long productId, List<StakeHolder> stakeHolders) {PolicyProduct product = productService.getById(productId);
// 其余填充数据,这里调用了聚合本身的动态工厂办法
Policy policy = Policy.create(product, stakeHolders);
return policy;
}
}
- model 中是畛域对象的定义。其中 vo 包中定义了畛域内用到的值对象。能够看到这里有 PolicyProduct 这样一个保险产品类,在投保畛域,咱们关注的是和保单相干的某个产品及其快照信息,因而咱们在这里定义一个保单保险产品类,防腐层负责把从产品域取得的保险产品信息转换为咱们关怀的保单保险产品类对象。
依照畛域驱动设计的最佳实际,畛域对象模型中不容许呈现 service、repository 这些用以获取内部信息的货色,它的外围概念是一个齐备的实体初始化实现后,它能做什么,或者它经验了什么之后状态会产生怎么的变动。
上面是畛域内外围的聚合 Policy 的示例代码。
@Getter
public class Policy {
private Long id;
private PolicyProduct product;
private List<StakeHolder> stakeHolders;
private Date issueTime;
/**
* 工厂办法
* @param product
* @param stakeHolders
* @return
*/
public static Policy create(PolicyProduct product, List<StakeHolder> stakeHolders){Policy policy = new Policy();
policy.product = product;
policy.stakeHolders = stakeHolders;
return policy;
}
/**
* 保单出单
*/
public void issue(Long id) {
this.id = id;
this.issueTime = new Date();}
}
- repository 是仓储包,只定义仓储接口,不关怀具体实现,具体的实现交由基础设施层负责,体现了依赖倒置的思维。
- service 是畛域服务,它定义一些不属于畛域对象的行为,然而又有必要的操作,比方一些流程管制。
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);
}
}
这里留神咱们注掉了一行 policyRepository.save(policy);,那么为什么要区别 save 和 create 呢?
save 是畛域驱动设计中最正确的做法:我的聚合或者实体有变动,仓储不必关怀是新建还是更新,帮我保存起来就好了。听下来很美妙,但对关系型数据库存储却是很不敌对的。因而,在咱们的场景里,须要违反一下书上所谓的最佳实际,咱们通知仓储是要新建还是更新,甚至如果是更新的话更新的是哪些列。
另外畛域驱动的最佳实际是基于事件驱动的,AxonFramework 对其有完满的实现,应用层收回一个 IssuePolicyCommand 指令,畛域层接管该指令,实现保单创立后收回 PolicyIssuedEvent,该 event 会被监听并且长久化到 event store 中。这种形式目前看起来在咱们这里落地的可能性不大,不做更多介绍。
5 基础设施层编程实际
1、分包构造
这里只展现了 repository 的实现,但实际上这里还有 RPC 调用的二方包实现类注入等很多内容。上文说到畛域层不关怀仓储的实现,交由基础设施层负责。基础设施层能够依据须要应用关系型数据库、缓存或者 NoSQL,畛域层是无感知的。这里咱们以关系型数据库为例来,dao 和 dataobject 等都能够应用例如 mybatis generator 等工具生成,畛域对象 和 dataobject 之间的转换由 convertor 负责。
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));
//... 其它数据入库
return policy.getId().toString();
}
@Override
public void updatePolicyStatus(String newStatus) {}}
这部分代码比较简单,无需赘言。
五 结语
对于畛域驱动,笔者仍处于初学者阶段,再好的设计,随着业务的倒退,代码也不免变得凌乱,这个过程中,每个参与者都有责任。最初,总结一下咱们维持代码初心的一些准则,和大家分享。
- 深刻了解业务场景,剖析用例,进行正确的畛域划分。
- 确定好实现形式后,大家尽量依照既定模式 / 格调编程,有异议的中央能够一起探讨后对立改变。
- 不引入不必要的复杂度。
- 一直对系统设计进行优化改良,对繁琐的代码,用设计模式进行优化。
- 写正文。
原文链接
本文为阿里云原创内容,未经容许不得转载。