基类与继承
-
领域模型中何时发掘和应用基类与继承:
1)基类概念必须是通用语言的一部分,并且对应领域中的一个抽象概念。
2)这个概念必须被进一步细化,才能具备实际业务含义。
3)基类概念被细化的子类存在着不同的变体(多态)
领域模型中的基类最好都定义为抽象类,即使它们并不包含任何抽象成员
在模型中用斜体标识模型的名称,或者用{abstract}关键字标识,在代码中也是在类前面添加abstract前缀表明该类为抽象类。抽象类中至少有一个没有任何代码实现的抽象方法 -
Liskov原则的内容如下:
子类必须可通过基类接口使用,而用户无须知道其差异。
子类应该能够替换其基类的任何实例,而不会产生问题 -
保证Liskov原则不被违反,需要做到:
❑ 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
❑ 子类可以增加自己特有的方法。
❑ 当子类的方法重载父类的方法时,方法的前置条件(即入参)要比父类更宽松。
❑ 当子类的方法实现父类的方法时(重写、重载、实现抽象方法),方法的后置条件(即输出、返回值)要比父类更严格或相等 -
模型设计的基本原则
(1)场景驱动原则
(2)低门槛原则
(3)自说明原则
上下文 -
上下文是一个语义上的边界,在一个上下文之内,每个领域概念的含义是明确的。同时,上下文也定义了模型的适用范围,清晰地定义了模型的用途(对应的用例),以及模型应该包含和忽略什么(有且仅有,内聚性)。它可以确保上下文之内的领域概念不会脱离其旨在解决的问题。它可以让团队明确模型的能力范围是什么
-
寻找语言的边界在某种程度上就是区别不同的用户群体
-
划分上下文的方法
(1)不同用户群,不同上下文
“不同用户群,不同上下文”的原理在于:一个用户群体只会使用一种语言描述一个概念,不会出现歧义。
因此,在用例描述中,不会出现术语定义不一致的情况,用例也就不会跨越不同上下文去实现
(2)不同商业价值,不同上下文
“不同商业价值,不同上下文”的原理在于:一种商业价值是以一组核心用例为代表,并由一组核心模型实现的。
交付一种价值对于模型的业务诉求是一致的。在这个过程中,用例中的术语及模型的定义和任务不可能有歧义。
(3)不同工作环节,不同上下文
“不同工作环节,不同上下文”的原理在于:当工作环节转换时,概念和模型的业务目标、所记录的信息、
的能力都发生了较大的变化
(4)不同技术栈,不同上下文 -
上下文是一个独立系统,具备从展现层到领域逻辑层、持久化(包括工厂和存储库),甚至到数据库的全部功能
-
把上下文定位为子系统是准确的,当然也可以把它定位于微服务,但那就是比较大粒度的微服务了
-
命名空间和包的命名方式,需要注意如下几点:
第一层要用公司名称,避免与另一家公司使用相同的名字。在厂商众多的复杂客户集成环境中,这非常重要。
要用稳定的、与版本无关的产品名称作为第二层,不要加版本号。
上下文和子系统的名称可以位于除第一层之外的其他层级,这取决于公司产品的商业设计架构。 -
在DDD中,持续集成分为以下两个层面。
(1)模型概念的集成
(2)代码的集成 -
CI通用的工作步骤如下。
1)签出代码:从源码管理系统中签出最新的代码到本地开发环境。
2)提交:基于主干分支创建一个新的功能分支,并在此分支编写代码,并向仓库提交代码。
3)自动化单元测试:代码仓库对每一次提交代码都会触发测试,单元测试将会被执行。一般来说,这些测试也会被打包到代码里。
4)编译:通过测试后,将源码转换为可以运行的实际代码,比如安装依赖、配置各种资源等。实现一个CI流程的唯一必要条件是有一个自动构建系统。源代码一般是自包含构建的,即CI流程所需的构建脚本是放在源码仓库中的。
5)接口和端到端测试:以自动化为主的全面测试,包括接口测试和端对端测试,测试的范围取决于团队制定的回归策略。
6)合并:通过测试后,将代码更新集成到主干。
7)回滚:如果当前版本发生问题,则回滚到上一个版本的构建结果。一般来说,CI服务器会配置成在遇到故障时发送邮件给相关人员,以便快速了解故障并采取纠正措施。 -
CI在DDD中的特殊之处。
对于领域层代码的签入应随时进行,改动不要在本地保留太久,不要超过24小时。另外,可以增加一个审核环节,审核通过后再合并到主干。审核有疑问要开会讨论,并叫上领域专家。
保持领域模型的独立与内聚,这将极大减轻CI和测试的工作量。系统最复杂的部分——业务逻辑,将只涉及领域层代码的合并。测试用例的编写、测试的运行也变得简单易行。
自动化测试对于CI的成功与否非常重要,领域专家和业务人员能充分参与单元测试用例设计,测试代码能体现通用语言,这是DDD的独特优势,一定要充分利用。
通用语言可对违规操作起到纠正作用。比如“订单判断是否超过合同规定的采购限额”,基于这句话,我们显然知道判断逻辑和限额都应该存在什么地方。一定要坚持使用通用语言。 -
跨上下文团队工作法
-
(1)上下文映射图
-
(2)集成方式
1)代码和数据库共享。这种方式会使得两个上下文之间产生比较强的耦合。但不可否认,它是最常见、最容易实现的集成方式。
相对于数据库的共享,共享代码模型可能更好。
2)基于RPC或RESTful的同步集成方式。这两种方式是上下文之间的同步调用方案
3)基于事件的异步协作方式。 -
(3)团队协作模式
1)共享内核(Shared Kernel)
它是指在其他集成方式成本非常高的情况下,多个上下文共享一部分模型以达到共享数据和逻辑的效果,这部分共享的模型就是共享内核。共享内核一般较稳定,模型可能是一个上下文的核心域,同时是另一个上下文的支撑域
2)消费者与供应商(Customer-Supplier Development Team)
上下文之间是一种单向依赖关系,比如处于上游的交易系统和处于下游的数据分析系统。处于上游的交易系统可能独立于下游系统,而下游系统的开发可能会受到很大的影响,如果不能形成上下游的有效协同,那么下游系统就必须遵从上游上下文的发布节奏来安排自己的工作。
3)追随者(Conformist)
当上下游团队不在同一个管理单元中时,上游团队没有配合下游的动力或可能性,下游团队对于上游的变更往往无能为力
这时,下游团队采用的就是追随者模式,完全借用跟随上游发布的模型或接口,下游团队可能必须牺牲其领域模型的清晰性,必须与上游的模型保持一致,通用语言也会因此发生变化,或者借用上游系统的上下文。
4)防腐层(Anticorruption Layer)
防腐层是我们推荐的团队协作模式,对于下游团队来说,即根据自己的领域模型创建一个单独的层,该层作为上游系统的代理向本上下文提供功能。防腐层承担两个上下文翻译的任务,这个层的实现可能不会很优雅,但是它可以让我们不牺牲自己领域模型的完美设计去换取对另一个上下文API的匹配。此外,除了一定的必要工作量,这个模式并没有任何副作用。
5)分道扬镳(Separate Way)。这个模式的含义是上下文之间完全独立开发、独立发布,不受外界任何影响,上下文团队不再纠结于与谁集成,而只通过接口的方式将自己的能力开放出去。
6)开放主机服务和发布语言(Open-Host Service and Published Langage)。类似于RPC、RESTful接口,都属于开放主机服务
重用性和稳定性:模块 -
模块之间应该是低耦合的,而模块内部则是高内聚的
-
模块可以分为两种:横向的和纵向的。横向的称为分层
-
包的内聚性原则
-
(1)重用和发布原则
重用和发布原则即包的重用粒度和发布的粒度相同。这一原则是从开发组件的角度来讲如何封装打包模型的,它包括以下3层含义:
❑ 模型是一个抽象的概念,为了重用一个模型,你必须将其放入一个可以重用的包中。同时,如果包是可重用的,那么模块中包含的所有模型都应该是可以重用的。
❑ 不以重用为目的的模型,不应该放入重用目的的包中,而要将其另行放置。
❑ 模型要同时被发布和跟踪。只有建立一个跟踪系统,为模型的使用者提供所需要的变更通知、安全性以及支持后,才有可能重用。
所谓重用,其实就是可以灵活组合来完成不同用例,包括封装的业务逻辑或技术能力。不具备重用性的类包括用户界面、数据访问层、数据结构等,因为它们的特殊性,一般不会被重用。可重用和不可重用的资源不要打包在一起。 -
(2)共同重用原则
共同重用原则即一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中所有的类 -
在领域模型中划分模块的一个依据是把参与同一个用例的模型打包在一起
(3)共同封闭原则
共同封闭原则即包中的所有模型对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中所有的类都产生影响,而对其他包不造成任何影响 -
包的耦合性原则
-
(1)无环依赖原则
无环依赖原则即在包的依赖关系中不允许存在环。当一个包中的类使用了另一个包中的类,这就产生了两个包之间的依赖。包之间的依赖关系应该是有向无环图,而不允许出现循环依赖的情况 -
(2)稳定依赖原则
稳定依赖原则即包之间的依赖关系必须朝着稳定的方向,被依赖者必须比依赖者更稳定。 -
(3)稳定抽象原则
稳定抽象原则即类越稳定,包就越抽象。一个完全稳定的包应该只由抽象类组成。
子域 -
子域更多是以层或微服务的形式出现的
-
按价值高低,子域可以分为核心域、支撑域和通用域。
1)核心域:最具价值、最专业的领域模型的集合,是系统区别于其他系统的独特价值点,体现的是组织的核心竞争力和独特的业务解决之道,如特有的商业模型或核心算法。
2)支撑域:支撑核心域周边业务的领域模型的集合,具有企业特性,但不具备通用性,如认证授权、查询浏览等。
3)通用域:通用功能的模型集合,通用的功能不具有企业特性,在不同的企业内部区别也不大,如邮件通知、日志记录等。 -
上下文对应的是闭环系统,而子域对应的是层或组件,模块则从重用性、稳定性进一步细分了模型和代码,它们划分模块的出发点有所不同。因此,正确的顺序是:先决定上下文,再确定子域和模块。上下文将帮助我们划分好用例、模型、代码和团队,之后我们可以从业务价值角度出发,逐步地提炼核心域、支撑域和通用域,再从重用性、稳定性和低耦合角度出发,考查模块打包的合理性。
子域和模块划分不应在上下文之前,但它们可以服务于多个上下文,尤其是在微服务架构中,此时上下文与子域的关系就是N:N的关系。
加粗样式
- 在DDD的实现环节,代码要做到:
❑ 忠于模型。
❑ 表达通用语言。
❑ 健壮性与灵活性。
❑ 良好的注释。
❑ 完备的单元测试。
- 将命名空间的设计与上下文保持一致。其下还可以划分子命名空间,以区分不同模块