1。前言与大家熟悉的MVC分层架构相比,领域驱动设计更适用于需要不断迭代的复杂业务系统和软件系统的架构模型。关于领域驱动设计的概念和优点,有很多文献可以参考。大部分同学都看过相关书籍,所以本文不讨论领域驱动设计。发展做一些简单的介绍。加入阿里健康后,我的团队也在积极推动领域驱动设计的应用。相关同学也给出了优秀的脚手架代码,但是目前看来实现情况并不理想。以我愚见,造成这个结果的主要原因有四个原因。MVC编程模式大家比较熟悉。当需要快速实现某个功能时,他们倾向于使用更安全、更熟悉的方法。大家对领域驱动编程应该怎么写没有统一的认识(AxonFramework[1]很好的实现了领域驱动设计,但是太“重”了)。DDD本身实现起来比较困难,往往需要事件驱动和EventStore才能完美实现,而这两个我们不常用。领域驱动设计是面向复杂系统的,业务开发初期看起来比较简单,领域驱动设计一上来就有过度设计的嫌疑。这就是为什么只有在必须重构系统时才经常讨论领域驱动设计的原因。笔者在研发过程中对领域驱动编程进行了研究和实践,对领域驱动框架AxonFramework也有深入的了解。(可能因为业务场景比较简单)当时落地效果还不错。不管从架构师的角度来看,从一线研发同学的角度来看,领域驱动编程的核心优势在于:实现面向对象的编程模型,实现高内聚、低耦合。在复杂业务系统的迭代过程中,保证代码结构不会无限混乱,从而保证系统的可持续维护。当然,领域驱动开发中最重要的是正确地拆解领域。这种拆解可以在理论的指导下,结合设计者的深入分析和对业务的充分理解来进行。本文假设在开发之前已经进行了领域划分,重点介绍如何在编码阶段进行实践,以体现领域驱动的优势。2.保险领域知识介绍以保险业务为例进行编程实践,如图所示为一个高度抽象的保险领域划分。通过用例分析,我们将整个业务划分为多个域(Bounded-Context),如产品域、核保、核保、理赔等,每个域又可以根据业务发展划分子域。当然,完整的保险业务要比图中显示的复杂得多。这里我们不作为业务知识的章节来介绍,只是为了方便后续的代码实践。三、领域驱动开发的代码结构1、领域驱动代码分层可以使用不同的Java项目发布不同的微服务来隔离领域,也可以使用同一个Java项目中的不同模块来隔离领域。这里我们使用模块来实现域隔离。但是无论采用何种方式进行域隔离,域间的交互都只能使用对方的二方包或API层提供的HTTP服务,不能直接引入其他域的其他服务。在每个领域内,相对于MVC对应用程序的三层架构的拆分,领域驱动设计将应用程序模块分为如图所示的四层。(1)用户界面层负责直接面向外部用户或系统,接收外部输入,并返回结果。比如二方包的实现类,SpringMVC中的Controller,具体的数据视图转换器,通常都位于这一层。代码层经常使用的包名可以是interface、api、facade等,用户界面层的输入输出参数定义为POJO风格。用户界面层是一个不包含业务逻辑的轻型层。安全认证、简单的入参验证(如使用@Valid注解)、访问日志记录、统一的异常处理逻辑、统一的返回值封装都应该在这一层完成。用户界面层所需功能的实现由应用层完成,这里一般不需要进行依赖倒置。在编码的时候,这一层可以直接引入应用层定义的接口,所以这一层依赖于应用层。需要注意的是,虽然理论上用户界面层可以直接使用领域层和基础设施层的能力,但是建议大家在熟练使用之前采用严格的分层架构,即当前层只依赖于它下面的相邻楼层。(2)应用层应用层具体实现接口层所需的功能,但这一层并不实现真正的业务规则,而是根据实际用例协调领域层提供的能力。消息发送、事件监听、事务控制等建议在这一层实现。代码层面常用的包名可以是application、service、manager等,用于替代SpringMVC中的service层,将业务逻辑转移到domain层。(3)领域层领域层是面向对象的,主要用于反映和实现领域中对象的内在能力。因此,在领域驱动编程中,不允许领域层的编程实现依赖其他外部对象。领域层的编程是基于我们对领域中的对象所具有的固有能力,以及它在当前的业务场景中会表现出什么。对自己的能力有了一定了解之后,就可以直接码字了。比如我们刚开始接触面向对象编程的时候,经常遇到的一个例子就是鸟能飞,狗能游。假设我们的业务领域只关心这些对象的移动,我们可以做如下实现。publicinterfaceMoveable{voidmove();}publicabstractclassAnimalimplementsMoveable{}publicclassBirdextendsAnimal{publicvoidmove(){//试着飞System.out.println("I'mflying");}publicclassDogextendsAnimal{publicvoidmove(){//尝试游泳System.out.println("I'mswimming");}}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16。17.领域驱动编程需要具备这种实现对象的能力(拥塞模型),而不是像我们在MVC架构中经常使用贫血模型那样,在服务中编写业务逻辑。当然,即使采用这样的编程方式,距离实现领域驱动还差得很远,一些看似简单的问题可能会给我们带来巨大的不安感。比如,复杂的对象应该如何初始化和持久化?同一个东西存在于不同领域,关注点不同,应该如何抽象?当不同域中的对象需要彼此的信息时,它们应该如何获取?这些问题,我们也会在代码示例部分尝试给出一些参考解决方案。(4)基础设施层基础设施层为以上各层提供通用的技术能力,如消息的监听和发送能力,数据库/缓存/NoSQL数据库/文件系统的CRUD能力等。2.总结基于进一步分析领域驱动设计的每一层,更具体的层次结构如下。基于上述分层原则,上述保险领域可以参考的代码结构如下。我们将在下面的代码示例中详细解释每个子合约的概念和功能。4、领域驱动开发的代码理论上,DOMAIN不依赖其他层,是业务的核心。我们应该先写领域层的代码,但是由于我们对保险领域的知识缺乏,我们可能不知道保险单有哪些内在的能力;二是为了讲解方便,直接用一个用例来展示代码。1.用例用户在前端页面选择保险产品,选择自选保险责任,输入投保人/被保险人信息,选择支付方式(分期/一次性支付等),支付后提交保险申请;服务器接受保险请求->承保->签发保单->签发保单利益。这里,用例1是用例2的pre-usecase。我们假设用例1已经成功完成(在用例1中完成费率计算),只实现了用例2,用例2只是一个粗略的实现,只要能显示代码风格就可以了。2.用户界面层编程实践(1)分包结构其中client是insurance-client(公共二方包)的实现,web是rest-style接口的实现。(2)用例代码@AllArgsConstructor@RestController@RequestMapping("/insure")publicclassPolicyController{privatefinalInsuranceInsureServiceinsuranceInsureService;/***保险单签发*@paramrequest*@return保单ID*/@RequestMapping(value="/issue-policy",method=RequestMethod.POST)publicStringissuePolicy(IssuePolicyRequestrequest){returninsuranceInsureService.issuePolicy(request);}}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16。这里使用的输入参数和返回值的类是在应用层定义的。3.应用层编程实践(1)分包结构最外层接口面向具体的业务场景,可以根据业务发展进行分包。pojo包定义了应用层使用的各种数据类(上面的IssuePolicyRequest在这里)和传播到其他层时需要进行类型转换的转换器。tasks包中定义了一些定时任务入口。注意在领域编程的实践中,需要进行大量的类型转换,我们可以使用一些框架(如MapStruct[2])来减少这些类型转换给我们带来的繁琐工作。(2)用例代码:@Service@AllArgsConstructorpublicclassInsuranceInsureServiceImplimplementsInsuranceInsureService{privatefinalPolicyFactorypolicyFactory;私有最终StakeHolderConvertorstakeHolderConvertor;私有最终PolicyServicepolicyService;特点*当底层为分库分表时,可能需要通过其他手段来保证事务,或者将非核心操作从事务中剥离出来(比如生成数据库ID)*/@Override@Transactional(rollbackFor=Exception.class)publicStringissuePolicy(IssuePolicyRequest请求){//发行流程控制policyService.issue(policy);PolicyIssuedMessagemessage=newPolicyIssuedMessage();message.setPolicyId(policy.getId());MQPublisher.publish(MQConstants.INSURANCE_TOPIC,MQConstants.POLICY_ISSUED_TAG,消息);返回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.这里的代码展示了应用层对用例2的处理:必要时使用领域层的工厂类构造Policy聚合对于复杂的对象,需要使用类型转换器来转换应用层的数据类进入领域层的实体类或值对象。使用领域层服务控制下单流程,发送下单成功消息,其他领域听取感兴趣的消息后响应。4.领域层编程实践(1)分包结构这里,领域层有五个一级分包:anticorruption为领域防腐层,即当前领域需要时对其他领域的二方包的封装了解其他领域或外部信息。从代码层面来看,防腐层可以避免调用外部客户端时在域内进行复杂的参数组装和结果转换。工厂解决了复杂聚合的初始化问题。我们为外部调用设计领域模型,但是如果外部也必须使用如何组装这个对象,就必须知道这个对象的内部结构。这对caller开发很不友好。其次,需要满足复杂对象或聚合中的领域知识(业务规则)。如果让外界组装复杂的对象或聚合,领域知识就会泄露给调用者代码。需要注意的是这里主要是填写聚合或者实体需要的数据,不涉及对象的行为。所以这里工厂的核心作用就是从各处拉取初始化聚合或者实体所需要的外部数据:@Service@AllArgsConstructorpublicclassPolicyFactory{/***产品领域的防腐层服务*/privatefinalProductServiceproductService;/***from可以直接从各种数据源找到的前置数据填入policy*@paramproductId*@paramstakeHolders*@return*/publicPolicycreatePolicy(LongproductId,List
