本文首发于 vivo互联网技术 微信公众号
连接: https://mp.weixin.qq.com/s/gk-Hb84Dt7JqBRVkMqM7Eg
做者:张文博前端
领域驱动设计(Domain Driven Design,DDD)其实并不是新理论,你们能够看看 Eric Evans 编著的《领域驱动设计》原稿首版是2003年,距今已十余年时间。与如今的分布式、微服务相比,绝对是即将步入中年的“老家伙”了。java
直到近些年微服务理论被提出、被互联网行业普遍使用,人们彷佛又从新发现了领域驱动设计的价值。因此看起来也确实是由于微服务,领域驱动设计才迎来了第二春。mysql
不过我发现你们对DDD也存有一些误区,使其渐渐成了一门“高深的玄学”,随之又被你们束之高阁。我本人在过去两年多的时间里,研读过多本DDD相关的经典论著、也请教过一些资深DDDer,并在项目中实践过。程序员
不过在初步学习、实践以后我又带着疑问与本身的思考从新读了一遍相关的著述理论。逐渐领悟到DDD做为一种思想,其实离咱们很近。面试
我把本身的学习过程、思考编写成系列文章,与你们一块儿探讨学习,但愿你们可以有所收获,固然其中不正确的地方也欢迎你们批评指正。redis
同时,在文章中我也会引用相关的论著或者一些我认为不错的案例素材,权当是咱们对这些知识的详细诠释,在这里一并对这些DDD前辈的不倦探索表示感谢。spring
(DDD相关的经典论著)sql
DDD是什么?众里寻她千百度,蓦然回首,“DDD是一种能够借鉴的思想,而非严格遵循的方法论”。数据库
当咱们面向业务开发的过程当中,应该首先思考领域模型而不是如何建表。安全
我听过太多业务开发的声音,“面试造航母、工做拧螺丝”,平常工做就是建表写增删改查。为何会有这样的认知,其根源在于表驱动设计思想而非领域驱动设计。
前者只能增长数据库的表数量,然后者才会造成长期的、具备业务意义的模型,这样的系统生命力才更加长久。咱们也才能用工程的方法来编码,从编码转身为业务域的开发专家。
有不少关于领域驱动设计的论述中都并未明确咱们如何获得“领域”,只有合理的领域模型才能有效驱动设计开发。因此建好领域模型是关键,对于领域模型的思考与技术框架升级一样重要。我曾经在互联网部门分享过如何进行领域建模,也欢迎你们与我交流沟通,有兴趣的读者也能够重点阅读一下《UML和模式应用》相关章节。
在讨论DDD以前咱们先来讨论一下“解耦”,这个词是咱们在平常编码时候常常说起的词语。一个具备工匠精神的程序员必定会在代码审查阶段对一些巨无霸函数或者类进行拆分,使各部分的功能更加聚焦、下降耦合。
另外一方面,在架构方面咱们也会重视“解耦”,由于一个模块之间随意耦合的系统将是全部人的噩梦之源。所以,除了整洁的代码咱们还须要关注整洁的架构。
架构的三要素:职责明确的模块或者组件、组件间明确的关联关系、约束和指导原则。内聚的组件必定有明确的边界,而这个明确的边界必然做为相关的约束指导从此的发展。
分层架构是运用最为普遍的架构模式,几乎每一个软件系统都须要经过层来隔离不一样的关注点,以此应对不一样需求的变化,使得这种变化能够独立进行;各个层、甚至同一层中的各个组件都会以不一样速率发生变化。
这里所谓的“以不一样速率发生变化”,其实就是引发变化的缘由各有不一样,这正好是单一职责原则(Single-Responsibility Principle,SRP)的体现。即“一个类应该只有一个引发它变化的缘由”,换言之,若是有两个引发类变化的缘由,就须要分离。
单一职责原则能够理解为架构原则,这时要考虑的就不是类,而是层次。例如网络七层协议是一个定义的很是好的、经典的分层架构,简单、易于学习理解,最终被普遍使用进而大大推进了网络通讯的发展。
一般状况下,咱们会把软件系统分为这几个层:UI界面(或者接入层)、应用独有的业务逻辑、领域普适的业务逻辑、数据库等。
接下来,还有什么不一样缘由的变动呢?答案正是这些业务逻辑自己!在每一层内部,不一样的业务场景发生变化的缘由、频次也都不一样,不一样的场景咱们分别定义为业务用例。由此,咱们能够总结出一个模式:在将系统水平切分红多个分层的同时,按用例将其切分红多个垂直切片。这样作的好处就是对单个用例的修改并不会影响其余用例。
若是咱们同时对支持这些用例的UI和数据库也进行了分组,那么每一个用例使用各自的UI表现与数据库,这样就作到了自上而下的解耦。另外一方面,有层次就有依赖。在OSI协议中,上层透明的依赖下层。可是在软件架构中,咱们更强调“依赖抽象”。即组件A依赖B的功能,咱们的作法是在A中定义其须要用到的接口,由B去实现对应接口能力,这样就作到了可插拔,未来咱们能够把B替换为一样实现了接口能力的组件C而对系统不会形成影响。
分层架构中给人的感受是每一层都一样重要,但若是咱们把关注的重点放在领域层,同时把依赖关系按照业务由重到轻造成一个以领域层为中心的环,即演变为一种整洁的架构风格。这里不是说其余层不重要,仅仅是为了凸显承载了业务核心的领域能力。
整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。通常来讲,外圆的声明(包括方法、类、变量)不能被内圆引用。一样的,外圆使用的数据格式也不能被内圆使用。
整洁架构各层主要职能以下:
Entities:实现领域内核心业务逻辑,它封装了企业级的业务规则。一个 Entity 能够是一个带方法的对象,也能够是一个数据结构和方法集合。通常咱们建议建立充血模型。
Use Cases:实现与用户操做相关的服务组合与编排,它包含了应用特有的业务规则,封装和实现了系统的全部用例。
Interface Adapters:它把适用于 Use Cases 和 entities 的数据转换为适用于外部服务的格式,或把外部的数据格式转换为适用于 Use Casess 和 entities 的格式。
咱们把整洁架构的外部依赖按照其输入输出功能、资源类型进行整合。将存储、中间件、与其余系统的集成、http调用分别暴露一个端口。则会演变成下面的架构图。
“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”“系统能平等地被用户、其余程序、自动化测试或脚本驱动,也能够独立于其最终的运行时设备和数据库进行开发和测试”这是六边形的精髓。
该架构由端口和适配器组成,所谓端口是应用的入口和出口,在许多语言中,它以接口的形式存在。例如以取消订单为例,“发送订单取消通知”能够被认为是一个出口端口,订单取消的业务逻辑决定了什么时候调用该端口,订单信息决定了端口的输入,而端口为上游的订单相关业务屏蔽了其实现细节。
而适配器分为两种,主适配器(别名Driving Adapter)表明用户如何使用应用,从技术上来讲,它们接收用户输入,调用端口并返回输出。Rest API是目前最多见的应用使用方式,以取消订单为例,该适配器实现Rest API的Endpoint,并调用入口端口OrderService,固然service内部可能发送OrderCancelled事件。同一个端口可能被多种适配器调用,本场景的取消订单也可能会被实现消息协议的Driving Adapter调用以便异步取消订单。
次适配器(别名Driven Adapter)实现应用的出口端口,向外部工具执行操做,例如向MySQL执行SQL,存储订单;使用Elasticsearch的API搜索产品;使用邮件/短信发送订单取消通知。有别于传统的分层形象,造成一个六边形,所以也会称做六边形架构。
我愚昧的认为,DDD即业务+解耦。大道至简、多么熟悉的场景,由于这就是咱们在作的事情,只不过咱们可能过于关注使用了什么技术框架、用了哪些中间件、写了哪些通用的class。
实际上DDD如同辩证惟物主义思想同样,哪怕咱们在软件项目的某一个环节用到了,只要这个思想为咱们解决了实际问题就够了。咱们没有必要为了DDD而去DDD,咱们必定是从问题中来再回到问题中去。
借助DDD能够改变开发者对业务领域的思考方式,要求开发者花费大量的时间和精力来仔细思考业务领域,研究概念和术语,而且和领域专家交流以发现,捕捉和改进通用语言,甚至发现模型乃至系统架构层面的不合理之处。固然有可能你的团队中并无相关业务的专家,那么此时你本身必须成为业务专家。
一般来讲咱们能够将DDD的业务价值总结为如下几点:
你得到了一个很是有用的领域模型;
你的业务获得了更准确的定义和理解;
领域专家能够为软件设计作出贡献;
更好的用户体验;
清晰的模型边界;
更好的企业架构;
敏捷、迭代式和持续建模;
经过前面的论述,你脑海里面必定闪烁几个词语“领域模型”“解耦”“依赖抽象”“边界”。这些通用的分析方法必定是放之四海而皆有效的。因此我认为当你按照这几个原则进行思考的时候就已经在DDD的路上向前迈进了一步,接下来咱们结合界限上下文、Repository这两个最容易被你们所忽略的地方来进一步阐述。
在这些步骤都作完之后,你再决定接下来如何去编码开发。不过我敢确定,你在这个过程当中已经获得了不少高业务价值的东西。
接下来如何去实现,你能够根据实际状况。我以为战略DDD比战术DDD更重要,我想这就是DDD做为一种思想的神奇所在。如同金庸笔下的少林绝学易筋经同样,一套并没有明确招式的内功心法却能打遍武林。
领域中还同时存在问题空间(problem space)和解决方案空间(solution space)。在问题空间中,咱们思考的是业务所面临的挑战,而在解决方案空间中,咱们思考如何实现软件以解决这些业务挑战。
问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。所以,问题空间是核心域和其余子域的组合。问题空间中的子域一般随着项目的不一样而不一样,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估很是有用。子域容许咱们快速地浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
一般,咱们但愿将子域一对一地对应到限界上下文。这种作法显式地将领域模型分离到不一样的业务板块中,并将问题空间和解决方案空间融合在一块儿。
可是在实践中,这种作法并不老是可能的,想像一下,谁没有维护过“毛线团”系统,如今咱们就要借助界限上下文来安全的、合理的、快速的理顺这堆交织不清的关系。
不少书籍或者文章讲解DDD,老是说突出应该怎么构建代码包结构,使用什么技术框架。我认为这是不彻底适用的,因此我会花较多时间来阐述一下如何借助界限上下文来理顺这堆“毛线团”。
我直接使用了《实现领域驱动设计》的相关章节的配图,权当是我对这个图的注释吧。
遗留的电子商务系统是个典型的“大线团”,咱们按照经验将其在逻辑上拆解为:产品目录子域、订单子域、fa票子域,固然你也能够拆解出更多的子域,甚至将产品目录子域继续向下分解为类目子域、商品子域(虚线是逻辑子域)。另外还有一个专门用于库存管理的库存系统、以及用于销售预测的预测系统。
因为历史缘由电商系统里面也存在物流相关的业务逻辑,同时物流又不可避免的做用于库存逻辑之上。而每每最难以把握的就是这部分相交的地方,这才是实际的项目场景,咱们一般作法是将其归并为一个新的履约系统,做为一个支撑子域去辅助主要的电商系统。
固然,随着业务不断发展,咱们的履约模式(好比支持同城当日达、商家仓储发货、电商集货仓发货、退货等等)、库存类型(调拨库存、越库操做、临期库存、残次库存等等)愈来愈复杂,咱们考虑将其再向下分解为履约系统2.0、库存系统2.0。
核心就是咱们能够在概念上使用多个子域来分解较大的界限上下文,也能够将多个分散的界限上下文包含在同一个新的子域当中,最终作到“子域和界限上下文一一对应”。我我的以为,这个过程是最考验内功心法的地方。
上面咱们已经说了会拆解出来新的子域,目的使“整洁干净”的界限上下文可以一对一的解决这个子域对应的问题空间,可是随着拆解就必然致使“关联关系”。由于要解决问题空间,必须使用对应的子域,你能够把它拆解出去,可是它始终存在于依赖网中。
咱们通用的作法是在相交的地方,定义接口。由支撑的界限上下文去实现,能够作到支撑上下文的插拔式切换。这里仍然是咱们强调的“依赖抽象”“解耦”。
“对于每种须要进行全局访问的对象,咱们都应该建立另外一个对象来做为这些对象的提供方,就像是在内存中访问这些对象的集合同样。为这些对象建立一个全局接口以供客户端访问。为这些对象建立添加和删除方法……
此外,咱们还应该提供可以按照某种指定条件来查询这些对象的方法……只为聚合建立资源库”引用自《领域驱动设计》。你们和个人疑问同样,Repository是什么?DAO与Repository什么区别?为何须要Repository?
首先,Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。
它的存在让领域层感受不到数据访问层的存在,它提供一个相似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层须要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不须要知道东西实际放在哪。其核心仍是“解耦”,因此咱们应该明确领域层只应该使用Repository获取对象。
接下来,看看DAO与Repository什么区别。
个人理解是这样,你能够将Repository看成 DAO 来看待,可是请注意一点,在设计Repository时,咱们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于你将本身的领域看成模型来看待,而不是 CRUD 操做;Repository是面向领域的,Repository定义的目的不是DB驱动的,Repository管理的数据的最小粒度是聚合根,这两点和DAO有很大不一样。
一般咱们建议把Repository定义为一个集合而且只提供相似集合的接口,好比Add,Remove,Get这种操做。一言以蔽之,咱们要用集合的思想来操做聚合根,而不是传统的面向DB的CRUD方法。
最**后来看看为何须要Repository,我理解仍是“解耦”。**当咱们把Repository想象成一个资源库,也不关心背后的持久化,这些也不是DDD该思考的东西,咱们能够用mysql来实现,也能够用mongo,甚至redis。尤为是当咱们在更换底层存储时候,领域层以及相关的服务并没有任何影响。
如下是代码示例:
package zwb.ddd.repository.sample.domain; import zwb.ddd.repository.sample.domain.model.BaseAggregateRoot; import java.util.List; /** * BaseAggregateRoot领域模型的基类,BaseSpecification适用于较为复杂的查询场景。 * @author wenbo.zhang * @date 2019-11-20 */ public interface IRepository<T extends BaseAggregateRoot, Q extends BaseSpecification> { T ofId(String id); void add(T t); void remove(String id); List<T> querySpecification(Q q); }
实现类:
package zwb.ddd.repository.sample.infrastructure; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import zwb.ddd.repository.sample.domain.IRepository; import zwb.ddd.repository.sample.domain.BaseSpecification; import zwb.ddd.repository.sample.domain.model.BaseAggregateRoot; import zwb.ddd.repository.sample.domain.model.Customer; import zwb.ddd.repository.sample.domain.model.CustomerSpecification; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author wenbo.zhang * @date 2019-11-20 */ @Component public class CustomerRepository implements IRepository { /** * Repository其具体实现上层是无感知的,若是之后咱们要切换为redis、mysql只须要修改这一层便可。 */ Map<String, Customer> customerMap = new ConcurrentHashMap<>(); @Override public Customer ofId(String id) { return customerMap.get(id); } @Override public void add(BaseAggregateRoot aggregateRoot) { if (!(aggregateRoot instanceof Customer)) { return; } Customer customer = (Customer) aggregateRoot; customerMap.put(customer.getId(), customer); } @Override public void remove(String id) { customerMap.remove(id); } /** * 咱们在Specification里面定义更加复杂的查询条件 * * @param specification 此处举例:基于id批量查询 * @return */ @Override public List<Customer> querySpecification(BaseSpecification specification) { List<Customer> customers = new ArrayList<>(); if (!(specification instanceof CustomerSpecification)) { return customers; } if (CollectionUtils.isEmpty(specification.getIds())) { return customers; } specification.getIds().forEach(id -> { if (ofId(id) != null) { customers.add(ofId(id)); } }); return customers; } }
在平常项目中咱们使用mybatis,因此在Repository中会使用mybatis的DAO来进行操做,下图是一个涉及到订购的复杂场景。
咱们举一个加盟业务来描述一下界限上下文的划分,以下图业务流程应该比较清晰,可是涉及一些术语,所以先把重要的术语定义清楚、下降你们的认知差别。
通用术语:
进件:金融领域术语,进件是指把资料准备好后提交给贷款公司或银行的系统里面,叫作进件,进件后银行或贷款公司就会开始审核这个贷款了。
上图的1.0版本,银行卡、进件、结算规则都跨越了问题域,所以咱们对其抽象“支付”“特约商户”上下文,以下图。
这里有人会有疑问,“特约商户”“商家”什么关系,是否应该把“特约商户”归属为“商家域”,这只是字面意思的类似,“特约商户”是进件审批之后造成的支付相关的业务。固然“商家域”会使用到“特约商户”的能力。
由于进件逻辑复杂所以咱们以进件为中心来画出了这样的上下文。另外一方面从状态流转来讲,“银行进件”是一个重要节点,表明平台、商家的一些权益即将生效,所以以此为核心也是有必要的。
随着店铺外卖团购业务的发展,咱们须要一个领域能力更丰富的履约安装域,可以进行社区配送、售后维修等。不可避免地将与订单、fa票、库存、售后等业务都有关系,所以以订单为中心构建了下面的上下文。
考虑到篇幅以及内容繁多,领域层相关的内容会在后面的文章中继续讲解。
本文主要讲述了战略层面的DDD原则,相对来讲较为抽象,但这是最考验内功、最不可忽视的环节。
再次强调一点,实践DDD毫不是参照一套网上的代码结构,依葫芦画瓢去重写本身的系统,这必定是失败的。建议你们按照本文所讲述的原则、方法去思考本身的系统,当你领悟其精髓之后必定可以“笑傲代码”,掌握解决软件核心复杂性的内功心法。