基本概念:
1.实体(Entity):
通常实体具备唯一id,能够被持久化,具有业务逻辑,对应现实世界业务对象。
实体一般和主要的业务/领域对象有一个直接的关系。一个实体的基本概念是一个持续抽象的生命,可以变化不同的状态和情形,但总是有相同的标识。
2、值对象(Value Object)
值对象的定义是:描述事物的对象;更准确的说,一个没有概念上标识符描述一个领域方面的对象。这些对象是用来表示临时的事物,或者可以认为值对象是实体的属性,这些属性没有特性标识但同时表达了领域中某类含义的概念。
通常值对象不具有唯一id,由对象的属性描述,可以用来传递参数或对实体进行补充描述。
关于实体与值对象的一个例子:比如员工信息的属性,如住址,电话号码都可以改变;然而,同一个员工的实体的标识将保持不变。因此,一个实体的基本概念是一个持续抽象的生命,可以变化不同的状态和情形,但总是有相同的标识。
3、聚合(Aggregate)
聚合是用来定义领域对象所有权和边界的领域模式。聚合的作用是帮助简化模型对象间的关系。聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。
聚合的特点:
① 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体
② 聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始访问,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持对它的引用的唯一元素
③ 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的
④ 聚合根负责与外部其他对象打交道并维护自己内部的业务规则;
⑤ 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象;
⑥ 聚合内部的对象可以保持对其他聚合根的引用;
⑦ 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念。
4、聚合根(Aggregate Root)
一个聚合是一组相关的被视为整体的对象。每个聚合都有一个根对象(聚合根实体),从外部访问只能通过这个对象。根实体对象有组成聚合所有对象的引用,但是外部对象只能引用根对象实体。
只有聚合根才能使用仓储库直接查询,其它的只能通过相关的聚合访问。如果根实体被删除,聚合内部的其它对象也将被删除。
通常,我们把聚合组织到一个文件夹或一个包中。每一个聚合对应一个包,并且每个聚合成员包括实体、值对象,domain事件,仓储接口和其它工厂对象。
5、工厂(Factory)
工厂用来封装创建一个复杂对象尤其是聚合时所需的知识,作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。当创建 实体和值对象复杂时建议使用工厂模式。
不意味着我们一定要使用工厂模式。如果创建对象很简单,使用构造器或者控制反转/依赖注入容器足够创建对象的依赖。此时,我们就不需要通用工厂模式来创建实体或值对象。
每个创建方法都是原子的。一个工厂应该只能生产透明状态的对象。对于实体,意味着创建整个聚合时满足所有的不变量。
一个单独的工厂通常生产整个聚合,传出一个根实体的引用,确保聚合的不变量都有。如果对象的内部聚合需要工厂,通常工厂方法的逻辑放在在聚合根上。这样对外部隐藏了聚合内聚的实现,同时赋予了根确保聚合完整的职责。如果聚合根不是子实体工厂的合适的家,那么继续创建一个单独的工厂。
域(Domain)
子域(Sub Domain)
界限上下文(Context)
资源库(Repository)
领域服务
领域事件(Domain Event)
数据传输对象(DTO Datatransfer Object)
层级模型:
4层模型:
接口和展现层Interface
应用层:Application Message & Event & Command
领域层:Domain
基础架构层:IoC Cache Presistence MQ Mail Log
业务的快速变化驱动着软件系统越来越复杂,DDD是为了解决特别复杂而且快速变化的业务系统。
比如一个选课系统,有两种实现方式:
1、使用MVC设计
在一个XkController中注入一个XkService,然后调用选课方法,在XkServiceImpl中注入StudentDao、CourseDao、TeacherService,然后有一个选课的xk()方法,在这个里面的学生实体Student、课程实体Course实体都用到了贫血模型,贫血模型不涉及到任何的业务逻辑,里面只有一些属性以及get和set。这种方式和数据库的交互是最方便的。因为实体类里面的属性是和数据库中表是一一对应的。
这种方式中,当业务逻辑(业务流程)发生改变的时候controller中的业务逻辑代码也要相应的改变。
2、另外一种方式:
controller中注入查库的service、选课的service等,然后写一年级选课serviceImpl实现选课service、二年级选课serviceImpl实现选课service。在这种方式中使用了充血模型,充血模型就是在实体类中不仅有属性和get、set,里面还有一些业务逻辑,比如Student实体类中还可以有study()方法、run()方法等,只要跟Student相关的业务逻辑都可以封装到实体类中。
在这种方式中,所有的业务逻辑全部都在service层,所有的业务实体都在domain层。选课方法中调用这些方法,当业务逻辑(业务流程)发生改变的时候controller中的业务逻辑代码不用改变,只需要修改相应的实现类的代码,增加删除也对controller没有影响,在controller中只需要换具体的实现就行了。
对比:
1、业务逻辑清晰度
就是业务人员去看代码就能大概的看懂代码写的是什么逻辑。在第一种方式中,业务人员看controller中的代码,当看到service层的代码时需要进入到service层去看里面的逻辑,而第二种方式中,业务人员只看controller层就能看懂整个业务逻辑,因为controller层都是调用的方法,从校验输入条件、读取数据库、选课、将数据更新到数据库,这样整个流程是很清晰的,业务人员不需要深入到每一层去看里面的逻辑。这就是业务逻辑清晰度。
2、DDD中防腐层:
在上面的选课系统中,把可能会产生变化的一些具体的实现单独抽取出来,做成一个单体服务或者微服务,比如checkService接口,然后在这个接口下可以有今年的选课规则实现类去实现这个接口,也可以有明年的选课规则实现类去实现这个接口,还可以有一年级的选课规则实现类去实现这个接口等等。在编程的时候只是用checkService去编程,无论下面有多少实现类,有多少不同的选课规则,但是checkService不会变,整个系统就不会腐化,永远都能用,这就是DDD的防腐层。
贫血模型和充血模型的区别:
贫血模型中的student实体,里面只有属性没有方法,这个student实体类会被各种service或者业务逻辑调用,比如选课、请假、体育、借书。这样的话,这个student实体类分布在各种各样的业务里面,现在如果要对student中的代码进行修改或者进行重构,就会很麻烦并且会有各种问题,必须把每个调用该student实体类的地方都修改。也可以将student看作一个微服务,其他的很多微服务都会用到这个微服务,当要修改student时,会对其他所有的微服务产生影响。
充血模型中的student实体,里面不仅有属性,还有一些方法,比如study()、run()、upgrade()等等和自身状态相关的业务逻辑,
充血模型的两个重要的概念:
1、两个充血模型之间怎么交互?
在两个充血模型之间的交互上又会有两个概念,叫做域(Domain)和领域服务(Domain Service):
在这个例子中,学生student实体和课程course实体就是两个不同的域,域就是领域的意思。我们对于域的封装不要受到其他域的干预,意思就是对于student的封装,里面只有跟学生相关的属性和方法,不要有跟课程相关的东西,这叫做不要受到其他域的干预,这样的话每个域就可以独立发展。但是两个域之间也是需要进行交互的,比如学生可以选择某门课程,学生也可以退选某门课程等等,随着业务的不断发展,两个领域之间的联系可能会不断地产生变化。让两个不同的域产生联系,可以使用一个service,比如选课xkService(studentId, courseId),比如退选txService(studentId, courseId),这两个service的作用是将两个不同的域建立联系,这个service就叫做领域服务。随着业务的发展,学生和课程之间可能会有新的业务逻辑(关系),这时只需要增加新的领域服务即可,对两个域没有任何影响。
商品域和用户域的例子更明显。
2、充血模型怎么和数据库交互?
在MVC中使用的是贫血模型,实体类中的每个属性和数据库中表的每个字段相对应。在DDD中使用的是充血模型,可以使用一个repository来管理一个实体的存储。比如用仓库StudentRepository来管理Student的存储,仓库StudentRepository也是一个防腐层,它可以有MySQL的repository或者Oracle的repository去存储,还可以分成两个表或者三个表,但是这些在StudentRepository中并不关心这些,它只关注我怎么取出一个Student对象。
此外仓库中可以用工厂Factory/Builder应对复杂对象的组装。比如在Student中除了有姓名、年龄、性别等基础属性,还可能有一些复合属性,比如address,这个address有可能在其他数据库中存着,这时使用Factory或者Builder将address从其他地方取出来,组装到Student中,然后返回一个整的Student对象。
1、服务(Service):
标识的是在领域对象之外的操作与行为,接收用户的请求和执行某些操作。
当用户在系统界面中进行操作时,会向系统发送请求,这时“服务”去接收用户的这些请求,然后根据需求去执行相应的方法。在执行这些方法的过程中,服务会去操作相应的实体与值对象,所有操作都完成以后,再将实体或值对象中的数据持久化到数据库中。比如当用户需要下单的时候就会从前端发起一个下单请求,该请求被订单service接收到,并执行下单的相应操作,在执行过程中,订单service会对订单实体中的数据进行校验,完成各种数据操作,最后将其保存到数据库中。
2、实体(Entity):
实体是通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象。
比如每个学生,都能用学生id来标识每个不同的学生个体,并且这个学生有姓名、性别、年龄等属性,这些属性会随着时间不断变化
3、值对象:
值对象代表的是真实世界中那些一成不变的、本质性的事务。这样的领域对象叫做“值对象”。比如地理位置、币种、行业。
实体和值对象区别:
可变性是实体的特点,不变性是值对象的本质。
比如:在线订餐系统中,根据业务需求的不同,菜单既可以设计成实体,也可以设计成值对象。例如宫保鸡丁,是一个菜名,如果将其按照值对象设计,则整个系统中宫保鸡丁只有一条记录,所有饭店的菜单如果有这道菜,都是引用这条记录,如果按照实体进行设计,则是认为每个饭店的宫保鸡丁都是不同的,比如每个饭店的宫保鸡丁的价格都是不同的,因此将其设计成有多条记录,有各自不同的id,每个饭店都是使用自己的宫保鸡丁。
将业务领域模型转换为程序设计通常用两种思路:贫血模型和充血模型
将需要封装的业务逻辑放到领域对象中,按照充血模型去设计。
除此之外的其他业务逻辑放到service中,按照贫血模型去设计。
需要封装起来按照充血模型设计的业务逻辑:
① 如果在领域模型中出现了类似继承、多态的情况,则应当将继承与多态的部分以充血模型的形式在领域对象中实现。
② 如果在软件设计的过程中需要将一些类型或者编码进行转换,则将转换的部分封装在领域对象中,例如一些Boolean类型的字段。
③ 如果希望在软件设计中能更好地表现领域对象之间的关系,比如在查询订单时想要显示每个订单对应的用户,以及每个订单包含的订单明细。
④ “聚合”,在真实世界中那些代表整体与部分的事物。比如在订单中有订单和订单明细,一个订单对应多个订单明细,从业务关系来说它们是整体与部分的关系,订单明细是订单的一部分,没有了这张订单,它的订单明细就没有任何意义,这时在操作订单的时候就应该将对订单明细的操作封装到订单对象中,按照充血模型的形式进行设计。
4、聚合:
聚合表达的是真实世界中整体和部分的关系。当整体不存在时,部分就变得没有了意义。比如订单与订单明细,订单明细是订单中的一个属性,但是由于在关系型数据库中没有办法在一个字段中表达一对多的关系,因此必须将订单明细设计成另外一张表。这就需要以聚合的形式进行设计。将订单明细设计成订单中的一个属性,在创建或更新订单时,将不再单独创建订单明细,而是将订单明细创建在订单中;在保存订单时,应当同时保存订单表和订单明细表,并放在同一事务中;在查询订单时,应当同时查询订单表和订单明细表,并将其装配成一个订单对象。在删除订单时,直接删除订单对象就行了,至于如何删除订单明细,是订单内部的实现,外部的程序不需要关注。在这些过程中,订单被当成一个整体在进行操作,不需要再单独去操作订单明细,也就是说对订单明细的操作时封装在订单对象内部的,对于客户程序来说,去使用订单对象就行了。
5、聚合根:
聚合根是外部访问的唯一入口。外部程序不能跳过整体去操作部分,对部分的操作都必须要通过整体,这时整体就成了外部访问的唯一入口,就称为聚合根。这样的好处是当聚合内部的业务逻辑发生变更时,只与聚合内部有关,只需要对聚合内部进行更新,与外部程序无关,从而有效降低了变更的维护成本,提高了系统的设计质量。但是这样的设计并非都是有效的。比如,在管理订单时,对订单进行增删改时,聚合是有效的,但是如果要统计销量、分析销售趋势、分析销售占比时,则需要对大量的订单明细进行汇总和统计,如果每次对订单明细的汇总和统计都必须经过订单的查询,那必然会使查询效率变得极低。因此领域驱动设计通常适用于增删改的业务操作,但不适用于分析统计。在一个系统中,增删改的业务可以采用领域驱动的设计,但是在分析汇总的场景中,直接使用SQL查询就行了。
6、仓库、工厂:
要想将聚合落实到软件设计中,还需要两个很重要的组件,仓库和工厂。
比如现在创建了一个订单,订单中包含了多条订单明细,并将它们做成了一个聚合,这时当订单创建完成之后就需要保存到数据库里,需要保存到订单表和订单明细表,并且在同一个事务中。在这个保存的过程中,之前使用贫血模型是通过订单Dao和订单明细Dao去完成保存的,然后由订单Service去添加事务,但是这样的设计没有聚合,缺乏封装性,不利于日后的维护。现在可以采用聚合的设计,将订单和订单明细的保存封装在订单仓库(Repository)中去实现,也就是说采用了领域驱动设计之后,通常就会实现一个仓库去完成对数据库的访问。
仓库就好比一个巨大无比的对象池,什么地方需要用到对象,就从池中拿就行了
工厂就是一个组装车间,因为有些对象是需要查多张表然后组装成一个对象的,那么查多张表和组装的过程就在工厂Factory中完成,
DDD四层架构规范:
1、领域中的对象由实体和值对象组成。对值对象的访问必须经由所属的实体对象
2、相关联的一组实体与值对象组成聚合。对聚合内对象的访问必须经由聚合根对象
3、跨实体的操作必须经由领域服务
4、应用服务层只通过领域服务或者聚合根来组织业务,自身不带任何实现逻辑
5、业务与数据隔离,领域层只关注业务,数据支撑全部交由基础设施层。