提取码:45on
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用领域驱动设计,这个问题本身就比较复杂(或者说这并不是一个好问题)。或许,更精确的提问方式应该是:“我应该选择什么样的架构风格来构建我的系统?”。现在我们先不急着回答这个问题,还是回到领域驱动设计的话题上,来回顾一下领域驱动设计里的基本概念。
领域驱动设计 很多人都了解测试驱动开发(TDD)、功能驱动开发(FDD)、API驱动开发(ADD)和行为驱动开发(BDD),那么什么又是领域驱动设计(DDD)呢?DDD的第三个D为什么是“设计”而不是“开发”呢?领域驱动设计最开始提出来的目的是为了简化业务人员与开发团队之间的沟通,以保证开发出来的软件产品不仅能够很好地解决业务领域问题并满足客户的需求,而且还能够简化或解决传统软件开发过程中遇到的各种问题(比如需求变更、横向或纵向扩展性差等等)。因此,通用语言(ubiquitous language)就是领域驱动设计中最重要最核心的概念:它能够确保代码的组织方式能够直接反映业务模型和业务逻辑,并且在整个业务系统中,对于同一个业务概念使用相同的代码表述(比如银行系统中的Account对象)。从通用语言的定义出发,领域驱动设计对于业务领域建模提供了一些指引,具体表现为引入了实体(Entity)、值对象(Value Object)、服务(Service)、聚合(Aggregate)、聚合根(Aggregate Root)、工厂(Factory)和仓储(Repository)。这里我就不打算深入讨论这些概念了,就简单回顾一下吧。
领域建模三剑客:实体、值对象和服务 在进行领域建模时,领域驱动设计引入了三个概念:实体、值对象和服务。实体和值对象都能够反映真实世界中的一个业务概念,两者的区别是,实体通过特定的标识符(ID)来确定一个个体,而值对象则是通过对象本身各个字段的值来确定一个个体。例如,某班的学生信息,学生(Student)就是一个实体,在进行领域建模的时候,一般会使用学号作为学生的ID,因为没有任何一个或者一组学生身上的属性能够唯一确定一个学生:姓名不行,出生日期不行,身份证号也不行(撇开有可能重号不说,用身份证号来标识学生会带来信息泄露问题);再比如学生的联系地址(Address)则是一个值对象,因为系统可以通过国家、省份、城市、街道和门牌号这些值的组合来唯一确定一个地址。 为实体设计一个合理的标识符(ID)策略,通常情况下并不是一件简单的事情:标识符需要具备全局唯一、生成高效、存储友好、意义鲜明这些基本特质,所以,Guid并不是一个很好的选择:它全局唯一、生成高效,然而并非存储/索引友好,而且是一串字符加数字和横杠,不代表任何意义。很多应用系统会有专门的服务来产生满足条件的标识符,比如销售系统很有可能会有单独的分布式服务来生成一个由订单日期、客户ID、订单流水号以及校验码组成的一长串字符串来用作订单编号。总而言之,为领域模型中的实体对象实现一个标识符的生成机制可以有很多种方法,这里也不进一步展开了,但是你会发现,领域驱动设计在这里只告诉你,实体需要一个ID,如何实现?这不是领域驱动设计的讨论范畴,因此也就回答了上面“第三个D为什么是‘设计’而不是‘开发’”的问题。 由于领域模型中的对象都是对业务概念的真实反映,所以,对象不仅会有状态,而且还会有行为,应该尽可能地将业务行为设计到合理的领域模型对象上,而不是将领域模型对象全部都设计成POCO/POJO,然后将所有业务行为都塞到 Transaction Script里。例如:学生会有写作业的行为,因此,doHomeWork(Homework homework)方法就应该设计在“学生”实体上。然而,有些情况下,某些业务行为很难归结到某个实体或者值对象上,一个经典的例子就是银行业务里的转账(transfer)方法,它并不是某个银行账户(Account)的行为,可能是银行的行为,也可能是用户的行为,在这种情况下,领域驱动设计引入了服务的概念:在服务上定义从领域角度无法归结到任何一种模型对象上的行为。由此可见,服务是领域建模中的一部分,也是领域模型的重要组成部分。
生命周期双子星:工厂和仓储 有了领域对象,自然就需要管理对象的生命周期,在介绍工厂和仓储之前,先看一下与领域对象相关的两个抽象概念:聚合与聚合根。聚合是能够表达一个完整的领域概念(或者说业务概念)的实体和值对象的组合,如果用UML类图来表示聚合,应该选择使用组合模式。不难理解,聚合里的所有实体和值对象都有相同的生命周期,它们被同时创建,也被同时销毁。对于每一个聚合,必定有一个实体其本身就代表了整个聚合的业务意义,比如“销售订单”聚合可以由“销售订单”实体、“销售订单明细”实体以及“联系地址”值对象组成,而其中的“销售订单”实体就代表了整个聚合的业务意义,像这样的实体,我们称之为聚合根。当然,有些聚合仅包含一个实体,而这个聚合的聚合根就是这个实体本身。所有与生命周期相关的操作都应该发生在聚合根上。 在领域驱动设计中,工厂负责创建聚合,而仓储负责聚合的持久化、激活以及销毁,这些操作都是应用在聚合根上。同样,领域驱动设计并没有讨论工厂和仓储应该如何实现,然而基于它们本身的特点,在实际中我们更多地会选择一些创建型模式来实现工厂,而选择一些数据持久化机制(比如数据库)来实现仓储。就仓储的实现而言,我们基本上会结合底层的数据存储技术选型来决定仓储的设计,甚至会将其抽象成 仓储设计模式。在不同的架构风格下,仓储的职责也会有所不同:传统分层架构下,仓储是有查询职责的,因为它需要基于聚合根来重建整个聚合,然而,在基于事件的CQRS架构中,仓储的查询职责变得非常薄弱,这是由于读写分离造成的。 以上基本上对领域驱动设计的基础性内容进行了回顾,如果你的项目正在,或者将要遵循上面的这些概念和指引进行业务分析与领域建模,或者在进行需求分析的时候,你的团队也在不停地考虑如何在软件中设计你所要面对的这些业务对象,并且在不停地梳理相关的领域知识,那么恭喜你,你已经步入了领域驱动设计的正轨。当然,在领域模型建立的过程中,你会发现很多问题,比如你会发现,银行账户与互联网登录账户都叫“账户”,但它们却是完全不同的东西;你甚至会发现,虽然都是“银行账户”,但在不同的场景下它所表述的意义完全不同(例如用于支付的支付账户与用户的定期账户是两码事),对于这些问题,领域驱动设计也提出了相应的解决方案,比如引入“界定上下文(Bounded Context)”的概念,而这一概念也刚好契合了目前最流行的软件架构风格:微服务架构风格,下文再深入讨论。 接下来你可以考虑本文刚开始的问题:我应该选择什么样的架构风格来构建我的系统。
软件系统架构风格 通常情况下,我们会选择一种软件架构风格来实现软件系统,而在开发的过程中,我们还会应用很多开发模式并且引入一些开发方法论,比如在模型持久化部分,我们会选择仓储模式,而在构建领域对象模型时,又有可能用到 访问者模式,我们还会选择使用敏捷开发方法论来指导我们的日常开发任务等等。由此可见,软件系统架构风格并非是一种模式,简单地说,架构风格决定了系统将由哪些组件组成,以及这些组件之间的关系如何,而架构模式则表述了如何实现这些组件以及处理它们之间的关系。 在 《面向模式的软件体系结构(卷一):模式系统》一书中,将软件设计模式分为三种:体系结构模式、设计模式以及惯用法。体系结构模式也就是架构模式,常见的有黑板模式、分层模式、MVC、发布者/订阅者、Proactor/Reactor、命令查询职责分离(CQRS)等等。这些模式的共同特点是,它们对软件系统的基本组织进行描述,这包括各种组件以及组件之间、组件与环境之间的相互关系的定义,并决定了软件系统设计与演进的原则。设计模式更多的是在组件内部,对于对象及其之间的关系以及它们之间的行为与协作提供一定的设计准则,从而使得组件的设计满足面向对象的SOLID原则。惯用法则是与特定编程语言相关的一种常用模式,比如在C#中,对于单例模式(Singleton)有它自己的独特的实现方式,这种方式依赖于C#中静态字段是线程安全的语言特性,而这种实现方式却并不能用在C++中。 与架构模式相比,架构风格并不关心真正的业务领域是什么,以及软件系统需要解决什么样的业务问题。无论你是开发ERP系统,还是开发购物网站,你都可以选择微服务架构,只是不同领域所需要的微服务不同罢了。常见的软件系统架构风格有:经典分层架构(N-Tier)、事件驱动架构(EDA)以及微服务架构(Microservices)。随着云计算的普及和推进,也衍生出了一些与云计算、人工智能以及大数据处理相关的架构风格,比如基于微软Azure云平台的 Web-Queue-Worker架构、 Big data架构以及 Big Compute架构。那么,我到底应该选择什么样的架构风格呢?在不同的架构风格下,领域驱动设计又如何运用呢?下面就对比较常见和流行的经典分层架构、事件驱动架构以及微服务架构做一些介绍。
经典分层架构(N-Tier Architecture) 这是一种为人熟知的架构风格,基本上所有开发人员都知道,软件系统需要分层设计。比较传统的常见的分层方式就是分三层:界面层、业务逻辑层以及数据访问层,各层之间会有数据传输对象(DTO)完成数据交互,以此隔离不同层内部的实现细节。领域驱动设计则将应用系统分为四层:用户界面层、应用层、领域层和基础设施层:
用户界面层:这一层比较好理解,就是直接面向用户的这一层,比如前端单页面应用或者基于MVC框架开发的前端应用。如果你的应用系统仅提供API,那么API这一层也属于用户界面层 应用层:根据领域驱动设计的描述,应用层是很薄的一层,它主要负责协调下层的执行任务,并隔离领域层与用户界面层。如果你选择采用经典分层架构,并开始实践领域驱动设计,那么在应用层你可以实现一些诸如Coordinator或者Workflow这样的组件,它们不参与任何领域或者业务相关的操作,仅仅负责协调。最常见的一种实现就是在应用层引入事务处理,有时候甚至还会跨资源实现分布式事务 领域层:你的领域模型所涉及的所有对象都会出现在这一层,如上文所述,领域层对象需要尽量避免贫血模型,开发团队与领域专家一起完成领域层的设计与开发任务 基础设施层:所有与技术细节相关的基础设施组件都属于这一层,因此,系统所依赖的数据库存储以及外部服务,都属于基础设施层。此外还有面向切面(Aspect-Oriented)的组件,比如异常处理模块、缓存模块、安全模块等等,也都属于基础设施层