领域驱动设计整理——概念&架构

领域、子域、限界上下文

DDD(Domain-Drive Design)的概念或者说业界的声音其实可以追溯到几十年前了。最近开始想要系统得整理一下DDD的一些东西。这一篇是一个简单的引子,也是mark一下自己接触到的概念和理解。
对于领域的概念其实很好理解,就如字面意思一样,比如出版书籍领域,广告设计领域。圈定了一定的范畴,并且在这个范畴内,所有团队成员对于某一概念的理解是一致的。比如书籍就是我们需要出版的书籍,而不是比如出版社给员工提供的什么季度奖励书籍。也许这里的例子还是有点偏差,但是当具体设计系统的时候就会去思考真正的业务边界。
我理解的领域驱动设计不是一种设计风格,也不是一种架构模型。而是一种思考方式。在考虑一个项目、需求的时候不会先去思考需要怎样的数据存储结构以及会有哪些行为和操作需要实现。而是去思考业务中一些重要的核心界限以及概念。最重要的就是定义通用语言。如何定义一个限界上下文的通用语言,来保证在具体的系统设计中,也即一个限界上下文中是一个唯一概念是非常重要的。限界上下文包含了模块(领域模型)以及上下文,所以它也是一个显示边界。
限界上下文中的术语一定准确反映通用语言限界上下文可以是我们的一个系统。比如最初,我们要给一个图书馆客户做一个系统,包含了图书馆的书籍管理,入库、借出。然后图书馆又提出,比如某些书籍是专门卖的,然后某些书籍是专门买进来奖励一些会员的奖品。然后我们需要确定,“书”这个概念,是不是已经引起了歧义。当我们的开发人员说出书这个词的时候,可能有的人理解是需要借的书,有的人理解是回馈客户的奖品。这个时候就出现了对于“书”的理解不一致,也就违反了领域模型的通用性。我们的系统边界,应该是完整的表达一个领域模型,或者说是通用语言。这就像音乐家创作的交响乐谱一样,多一个音符不多,少一个音符不少,刚刚好好,完整而优美得可以演奏出来一篇华丽的作品。所以系统设计也是一样,我们所说的领域专家其实就是具备能细致得划分领域边界,以及从复杂的业务中抽象出来通用语言的人。所以刚才的图书馆的例子,在落地到系统中的时候,就需要考虑系统划分了。出售书的系统中的书,不会使用租书系统中的书的实体来处理自己的领域事件。
在限界上下文定义好之后,就可以划定核心域了。比如这里,图书馆的核心业务是租书来收取定期的阅读费用。那么租书上下文中的租书服务就是我们的核心域了。图书馆的场景中,还需要有一个独立的用户系统,来管理用户登录和用户基本信息。租书上下文,售书上下文都会需要调用用户管理上下文。用户管理上下文就是一个通用子域。当然还有其他子域,有些子域可能共享一些状态,是一种协作上下文。

上下文映射

当我们需要设计多个上下文来完成需求的时候,就会要考虑上下文之间是否存在着一些联系。就如上述,用户管理上下文就是一个上游系统,成为其他系统的支撑子域。当我们需要在租书系统中需要用户信息的时候可以有几种做法:

  1. 共享内核方式:对于模型和代码的共享,但是这需要团队之间协作紧密,始终保持通用语言的一致性
  2. 客户-供应关系:上游团队的开发需独立于下游,并且也要尽量估计下游团队是需求和时间。
  3. 防腐层(Anticorruption Layer): 下游系统对上游模型进行翻译。防腐层的接口用来和其他系统交互,在防腐层内部,来实现模型概念的转换。
  4. 开放主机服务(Open Host Service):定义协议,让其他系统通过协议访问系统服务。
  5. 发布语言(Published Language):在两个限界上下文之间发布一个公用语言应用于翻译模型中。
    在实际的集成中我们可呢过需要结合多种映射方式。比如,如果使用分离内核(和共享内核相对的),可以在上游用户管理上下文中使用开放主机服务和发布语言,然后下游的借书上下文通过防腐层来进行模型的转换。这种上下文的映射保证了通用语言在所有模块的纯洁性。每个上下文的开发团队就只需要专注于自己上下文的领域和统一的语言环境中就可以了。
    在实际的实现手段上可以有多种方式,比如开放主机服务,可以使用Rest的方式,或者RPC 机制,或者消息机制。发布语言可以使用XML、Json、protocal buffer等或者消息的形式。防腐层就是把外界的概念翻译成本上下文的对象。在开发过程中也可以用一些概念映射关系来表达通用语言。团队成员共享这些概念,并且时刻提醒自己所关注的领域范围。上游上下文需要把资源不可用显示暴露出来,下游上下文需要能正确处理上游的模型状态。在实施中可以用消息机制,或者做一些异步的探测依赖的上下文的可用状态等。

DDD架构

终于到了正文。后面按照几种比较普遍的分类陈述。

依赖倒置

现在主流的框架中,依赖倒置简言之就是依赖接口、抽象,而不是依赖具体的实现。这种分层的形式,在现在的系统架构中都普遍流行。Java语言的Spring就是一个典型的践行依赖倒置的框架。这种基于抽象的分层,能让应用服务和领域服务很好的解耦。但是有时候过度得设计分层也会倒置贫血模型。贫血模型和反模式这种话题也是若干年没有讨论出一个谁是谁非的话题,这里就不展开了。其实,只要是对领域模型有正确的抽象,能反应出一套统一的通用语言,再根据具体的业务时间去选择自己的架构方式就足够了。
所以这里希望在实践的过程中,需要坚持不要过度依赖应用服务,让应用服务的抽象承担了太多的领域服务职责就可以了。另外,应用服务和领域服务是两个完全不同的概念。应用服务可以和其他上下文进行服务输出、输入、可以处理事务和复杂的业务逻辑。但是领域服务是更加轻量级的,为应用服务提供领域操作的。如果将计算和验证结合在实体中就是充血模式,如果是需要多个领域聚合的就可以再抽象出来一层单独的领域服务层。当然领域服务也不一定要用依赖倒置,或者说,业务领域服务根本不需要接口,因为领域专属的东西,不希望向客户端泄露扩展方式。

六边形

六边形的架构是很适合和领域驱动结合的架构方式。六边形架构的简单架构图如下:领域驱动设计整理——概念&架构
图片来自《实践领域驱动设计》。无论是SOA 或者是RESTful 都很适用六边形的架构。我觉得这里的六边形体现了一种平等性,就像蜂巢一样。每一个窝都是平等的,并且可以很好地交互。新增一个扩展的Client只要新增一个适配器就可以将client的输入转化成系统内部的API的参数模型。系统输出服务也是一样的。所以六便习惯的关键就是每个适配器。每个外界的类型都有一个适配器想对应。外界通过API和系统交互,可以使一个Http请求,也可以是消息机制。适配器将外界的请求或者内部的输出都通过API参数的形式来设计和交互。对于参数映射可以采用很多框架来帮助完成。六边形的内部就是领域模型的应用。通过适配器,能进行很好的系统间防腐,做到外界参数和领域模型的互相转换。Spring 框架对于Restful的参数映射和资源映射也有很好的支持。通过注解的方式,就可以完成适配器应该做的工作。具体的例子可以参看下Spring Annotation Based Controllers

SOA

SOA 架构图如下:
领域驱动设计整理——概念&架构 SOA结构中,服务的边界是在六边形的外层(这里的六边形依然是描述了单个上下文的结构)。在设计SOA 架构的时候,不应该以REST 或者SOAP 或消息类型来决定上下文的大小。这样会导致多个小的限界上下文和领域模型。所以在设计服务导向的上下文的时候需要注意保证上下文所要表达的通用语言的领域模型。

RESTFul Http

RESTFul 的架构中,每个资源都有一个URI,通过资源的方式来向外界提供操作和访问入口。Restful是具有”Presentation”和”State”的。
- 展现的格式可以是xml、json或者html,或者二进制的数据。
- 无状态表示的是一种请求的独立性,提高可上下文的可伸缩性。
对于资源确定之后就可以确定操作接口(如Http的get、post、put、delete等)。但是在设计接口中不应该将领域模型暴露给外界,不能因为领域模型的改变导致系统接口的变化。所以Restful 可以结合六边形架构。用Spring的web.bind服务就可以很好地实现这个上下文边界的设计。
为了和领域驱动很好的结合,还可以和微服的架构思想一起考虑,如果每个上下文都是一个微服,可以独立部署。那么在每个领域的限界上下文之间可以建立一个统一的系统接口层。所有需要多个领域上下文的请求都可以通过这个系统接口层。将核心领域和各个协作上下文的领域解耦。这种是对于各个上下文没有互相依赖的情况下来说是一种很好的服务输出方式,并且还可以用很多异步、并发控制框架来提高请求处理的性能。
对于上下文之间依赖,其实除了RPC,也可以通过事件驱动或者还是用Restful的方式进行集成。但是无论以什么方式或者技术手段来实现,都要注意不要将外部的领域模型暴露给本地系统,或者把本地限界上下文的领域模型暴露给外部。具体的处理方式,依据不同的架构会有不同的实现。如果是Restful 的方式,可以通过一个适配器(Adapter)来实现http 请求的转发,和对本地模型(DTO)映射的处理。然后将模型转化进一步交给翻译器(Translator),翻译器负责将远程对象(其他限界上下文的DTO,而非领域模型)转化为本地DTO,这个过程可以用Spring的RestTemplate(也可以进一步对于template封装一层本地服务的Facade)来帮助对HttpClient 的封装和JSON数据进行处理。

CQRS

CQRS 是命令查询的责任分离Command Query Responsibility Segregation的简称。其实可以简单理解为读(Query 请求)写(Commend请求)服务的分离。CQRS可以很好的解决复杂的界面显示的问题。一个接口或者是获取参数处理命令的,或者是返回数据的。这样的分离可以在读写两个层次上分别抽取出不同的系统服务。C和Q的数据可以通过领域事件的方式进行同步。CQRS一篇很好的总结可以参考一下CQRS架构简介

事件驱动

Event-Driven Architecture(EDA) 通过消息机制可以很好地完成上下文之间的解耦。当然在选择用消息集成的时候,对于可靠性和实时性上的要求需要做好权衡。事件驱动可以结合在六边形的架构和Restful架构中。作为一种辅助的解耦方式。本地的消息可以通过guava的Eventbus,集群的可以通过RabbitMQ来实现。
对于事件源来说,事件发布需要对于领域对象的修改进行跟踪,聚合上的每一次操作都有一个领域事件发布出去,每一个领域事件都需要被保存到一个时间存储中,这样可以保证对于事件发布前后的各个状态都能回溯。在实现最终一致性上,消息机制往往需要更加复杂的处理。订阅方也即Observer中的观察者需要对时间进行存储。当客户端需要查找聚合实例的时候,通过资源库再向事件存储中查找最终需要的聚合实例。当聚合实例发生变化的时候再执行实践的发布。围绕一个聚合的实例就形成了一个生产和消费的闭环。整个事件机制中还要考虑重发机制以及超时时间。订阅方需要幂等得处理事件,并且再状态不一致的时候,可以进行失败补偿。如果允许失败,那就可以直接采用工作流的方式。在具体的实施中可以有很多方式,都需要根据实际的场景和投入产出做具体的衡量。

数据网织

DataFabric 主要是在大数据处理上的一种架构方案,处理DB 性能瓶颈的时候可以将领域模型以序列化的方式放到缓存中,具体的保存方式可以是文本、json格式,也可以是二进制数据。可以通过很多Nosql技术来实现,GemFire、Coherence、redis、Mongo等。

结语

以上的所有架构都可以通过DDD的思想进行实现,在实际的系统架构上,肯定也不止是只应用其中的一种。六边形一般都可以结合Restful或者事件驱动。当然无论采用什么架构或者技术手段,回归到领域驱动的核心就是对于通用语言的定义。限界上下文要保证自己的纯粹性,尽量减少上下文间的遵奉关系或者共享内核。尽量保证系统的自治性,对其他系统的无感知性,不要将系统内部的领域模型暴露给客户端。后面会继续抽空对DDD的实施进行详细的总结和整理。

相关推荐