何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(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)的行为,可能是银行的行为,也可能是用户的行为,在这种情况下,领域驱动设计引入了服务的概念:在服务上定义从领域角度无法归结到任何一种模型对象上的行为。由此可见,服务是领域建模中的一部分,也是领域模型的重要组成部分。
生命周期双子星:工厂和仓储