DDD这几年越来越火了,资料也很多。大部分资料都偏向理论介绍。给出的一些代码与传统MVC的三层架构有很大区别。此外,过多的新概念很容易让初学者不知所措。本文从MVC架构的角度阐述了如何向DDD架构演进。从DDDCode的角度看MVC架构的问题:瘦实体模型:只起到数据类的作用,业务逻辑分散到服务上,可维护性越来越差;数据库表编程,而不是模型编程;entityclass它们之间的关系是一个复杂的网络结构,变成一个大泥球,牵一发而动全身,很难轻易改代码;服务类承担的所有业务逻辑越来越臃肿,很容易出现几千行的服务类;外部接口直接暴露了实体模型,导致内部逻辑不必要的对外暴露。即使有DTO类,一般也是实体类的直接拷贝;外部依赖层直接从服务层调用,大量的字段转换和异常处理充斥在服务的方法中;项目管理视角:交付效率:越来越低;稳定性差:难以测试,代码变更的影响范围难以估计;理解成本高:新成员介入成本高,长期会导致模块只有一个人最熟悉,离职成本非常高;第一层:初出茅庐的问题越来越严重,很多人开始把目光转向DDD,于是埋头看了几本大书,对以下几个概念有了基本的了解:UnifiedLanguageBoundedContextDomain,Subdomain,SupportDomainAggregation,Entity,ValueObject分层:UserInterfaceLayer,ApplicationLayer,DomainLayer,andBaseLayer于是MVC架构转型进化为DDD分层架构。DDD分层架构:从MVC架构到DDD分层架构的映射:至此,DDD架构已经基本介绍完毕,同时扩展性也得到了一定程度的提升。但是,随着业务的发展,新的问题不断出现:一段业务逻辑代码应该放在应用层还是领域层?领域服务被视为原始MVC中的服务层。随着业务的不断发展,班级也在不断扩大。好像一样?聚合包含多个实体类。这个接口没有使用那么多的实体。为了性能,最好直接写SQL返回必要的操作,但这似乎又回到了MVC模型,因为实体类可以包含业务逻辑和领域服务。业务逻辑在哪里?资料上说domain层不能有外部依赖,要做到100%的单测覆盖,但是我的domain服务需要使用外部接口,中央缓存等,所以没有外部依赖?第二层:曹传借箭(战术设计)不断学习别人做题经验,不断尝试,逐渐得到以下技能:1.领域层领域(domain)是一个模块,包括以下组件,传统的服务根据功能,可以拆分到任何地方,各司其职。1Aggregation1tomultipleentity几个值对象MultipleDomainServices1Factory:NewAggregation1Repository:聚合存储服务聚合根(AggregateRoot)聚合本身也是一个实体,聚合可以包含其他实体,其他实体离不开聚合提供服务,比如文章下的评论,评论必须从属于文章,没有文章就没有评论。仓库层(repository)也必须提供基于聚合的服务;实体:可以理解为数据库表,必须有主键;值对象:没有主键,依附于实体存在,比如用户实体下的地址对象,一般已经以json字符串的形式存在于数据库中;最常见的值对象是枚举;RepositoryService(存储库)资源库是一种聚合存储机制,外界通过资源库,也只有通过资源库才能完成聚合访问。存储库将对象作为一个聚合的整体进行管理。因此,一个聚合只能有一个存储库对象,即以聚合根命名的存储库。除此以外的对象不应提供资源库对象。存储服务的实现方式一般有两种:SpringDataJPA和Mybatis。如果用SpringDataJPA实现,直接使用JPA注解@OneToOne和@OneToMany,配合fetch配置,一次查询所有相关实体。如果是用Mybatis实现,那么repository需要添加多个mapper引用,然后手动组装。这是一个经典的Hibernate笛卡尔积问题。答案是在聚合根中,一般不会加入大量的关联实体对象。如果真的需要查询相关对象,而且相关对象很多怎么办?DDD中有一种CQRS(Command-QueryResponsibilitySegregation)模式,是一种读写分离模式。在这种场景下,需要将查询操作放在查询命令中进行分页查询。当然,CQRS也是一个非常复杂的模型。不要照搬别人的方案,要根据自己的业务场景选择适合自己的方案。下面列出了CQRS的几种应用模式:工厂服务的作用是创建聚合,只传入必要的参数,复杂的创建逻辑隐藏在工厂服务内部。简单的聚合可以直接通过new、static方法等方式创建,不一定是factory创建的。领域服务单个实体对象可以处理的逻辑放在实体中,多个实体或交互场景放在领域服务中。领域服务能否调用存储层或对外接口?可以,但不能直接和域服务代码放在一起。领域服务模块存储API,实现放在基础设施中。领域服务对象不建议直接用聚合名+DomainService命名,而是与操作命令关联起来,比如用户保存服务命名为:UserSaveService,审计服务命名为:UserAuditSerivce。2.应用层应用层通过应用服务接口暴露了系统的所有功能。在应用服务的实现中,它负责编排和转发。它将要实现的功能委托给一个或多个领域对象来实现。它只负责业务用例的执行顺序和结果的组装。这样,它隐藏了领域层及其内部实现机制的复杂性。例如下订单服务的方法:publicvoidsubmitOrder(LongorderId){Orderorder=OrderFetchService.fetchById(orderId);//获取订单对象OrderCheckSerivce.check(order);//验证订单是否有效OrderSubmitSerivce.submit(order);//提交订单ShoppingCartClearService.clear(order);//移除购物车中已购买的商品NotifySerivce.emailNotify(order.getUser());//发送邮件通知买家}对于复杂的业务,应用层也有几种模式:编排服务:最典型的如Drools;命令、查询命令模式;业务按Rhase和Step逐层拆分模式;3、Maven模块划分基础层是一个比较简单的层,但是这里还有一个疑惑问题:按照DDD的四层架构图来划分Maven模块。基础层是顶层,但基础层也包含基本组件供其他层使用。Maven模块可以创建循环依赖。相比之下,另一种架构图更准确,但仍然不能直观地反映出Maven模块是如何划分的。我的最佳实践是将基础层拆分成两部分,一部分是基础组件+存储API,另一部分是实现。maven模块划分图如下:第三层:strategicing(策略设计)经过以上两层训练,恭喜,你已经学完了DDD战术,应付日常代码开发已经足够了。但是,作为架构师,探索之路不能就此止步,接下来会讲到DDD策略部分。策略部分主要关注三个点:统一语言领域限界上下文1.统一语言统一语言的重要性可以用JeffPatton在《用户故事地图》中给出的一幅漫画来直观的描述:统一语言是精炼领域知识的输出因此,它也是后续需求迭代和重构的基础。统一语言的建立有以下几个要点:统一语言必须以文档的形式提供,并在整个项目组的各个团队之间达成共识;统一的语言名称必须有相应的英文名称,并且在整个技术栈中保持一致;统一语言必须是完整的,包括以下要素:领域模型的概念和逻辑;有界上下文(BoundedContext);系统隐喻;模式)和成语。2.领域划分采用事件风暴(EventStorming)的形式,列出所有用户故事(UseStory)。用户故事可以通过6W模型来构建,即Who、What、Why、Where、When和how来描述场景。元素。然后将功能相似的部分圈起来形成域,按功能不同分为核心域、支撑域和通用域。具体过程参考资料很多,这里不再赘述。最终输出的是领域划分图,下面是一个保险业务的例子:3.有界上下文有界上下文由两部分组成:Context是业务目标,Bounded是保护和隔离上下文的边界。比如上图中的implementation部分就是boundedcontext的边界,虚线部分代表domain的边界。限界上下文没有统一的划分标准,需要读者根据自己的业务场景来确定如何划分。一个上下文包含相同的领域知识,角色在上下文中完成动作目标;边界体现在以下几个方面:领域逻辑层:确定领域模型的业务边界,保持模型的完整性和一致性,从而降低系统业务复杂度;团队合作层:限界上下文一般是用户切换团队的依据;技术实现层:Boundedcontext可以看作是微服务的边界;DDD的缺点DDD架构是一种高级的方法论,在很多场景下都能发挥很大的价值,但DDD并不是灵丹妙药。资深架构师将DDD架构作为一种工具,结合其他架构经验为业务服务。DDD的缺点有几个方面:性能:DDD基于聚合来组织代码。对于高性能场景,在聚合中加载大量无用字段会严重影响性能。比如报表场景,直接写SQL会更简单直接;transactions:DDD中的事务仅限于有界上下文。跨多个限界上下文的场景,需要开发者额外考虑分布式事务问题;难度系数高,推广成本高:DDD项目需要领域专家和专家,需要特别熟悉业务,Modeling和OOP也很难让管理者评价一个人是否真的能胜任;综上所述,本文从MVC架构入手,讲述如何从DDD架构演进。限于篇幅,很多DDD知识点没有提及,希望大家在实践过程中灵活运用,享受DDD给业务带来的价值。本文如有不足之处,欢迎反馈。本文链接:从MVC到DDD的架构演进作者简介:穆晓峰,美团Java技术专家,专注于分享软件开发实践和架构思维。欢迎关注公众号:Java研发
