前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >领域驱动设计

领域驱动设计

原创
作者头像
kaiwest
修改2025-01-18 17:56:06
修改2025-01-18 17:56:06
15000
代码可运行
举报
运行总次数:0
代码可运行

本文对《领域驱动设计-软件复杂性应对之道》一书进行高度凝练,梳理了领域驱动设计的架构图、基本要素和重要概念。从细节入手,以小见大,你想知道的定义,这里都有。

图片
图片

用过DDD的同学会知道,自己用倒没什么,一旦到了要推广部门内使用的时候就会非常困难。为什么会这样?做为一个有8年DDD实战经验的过来人,我想尝试分析一下原因:

  1. 过多的概念让人望而生畏,这也是DDD饱受诟病的地方,在推广DDD的过程中我不止一次听到“DDD太复杂了,在我们这里没法落地”。
  2. 也是由于初始学习曲线过于陡峭,大部分人其实处于“懂”和“不懂”中间,这其实很可怕,人们在引入DDD的时候会只引入容易理解的部分,忽略掉自认为“没啥用”的部分,最后导致既没达到想要的效果又增加了复杂度,然后得出一个结论:DDD我用过,不好用
  3. 思维惯性和怕承担风险。人是倾向于使用已有的知识结构去解决问题的,用简单的分层架构,加一些存储和中间件,不用DDD好像也能解决问题。“又不是不能用”,大家都不愿意冒着风险引入一些新东西。

行吧,那我们姑且撇开那些枯燥的概念,只一篇文章也聊不明白(当然我也得假设你至少听说过这些概念,否则你不会点进来看这篇文章)。接下来我们就只探讨DDD的思想内核,或许能稍稍降低一些理解门槛,尽量让大家明白“哦,原来DDD是干这个的!”

一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。——架构整洁之道

就像《架构整洁之道》一书所说,如果一个系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。那么DDD战术设计推崇的架构模式是如何控制变更成本的呢?

我们知道,架构与底层工具应该是完全独立的,一个良好的架构应该围绕业务来展开,这样的架构设计可以在脱离架构、工具及使用环境的情况下完整的描述业务用例。并且,架构应该在满足业务需要的前提下,尽可能地允许用户能自由的选择工具。

此外,良好的架构设计应该尽可能地允许用户推迟和延后决定使用什么框架、数据库、web服务以及其它与环境相关的工具(例如spring、hibernate、mybatis、数据存储、消息组件等)。同时,良好的架构还应该让我们能很容易的改变这些决定。总之,良好的架构应该只关注业务用例本身,并能将它们与其它周边因素隔离开来

在最初设计时,多数分层架构为了层级职责清晰,会比较偏向严格分层架构。但是经过多次的转手和迭代后,严格分层架构会逐步演变成松散分层架构,各层职责越来越不清晰。比如可能为了方便,直接在controller里直接通过dao访问数据库;service的职责也越来越混乱,里面可能混杂了基础设施的远程调用、资源库获取等等不属于它的职责。长此以往,对系统的可维护性带来巨大的挑战,成员之间互相接手时需要花费大量的时间才能真正上手,随时都会有踩雷的风险,无法做到研发资源的最大化利用。

DDD的内核是什么

那么DDD的内核是什么呢?

  1. 指导写代码?显然不准确,格局低了一些。
  2. 指导做架构设计?说对了一半,没讲到精髓。

先来看一张架构图示例:

这是现在很典型的一种架构分层,横向先根据不同技术领域先分几层,大差不差也不会有什么人挑战。问题出在应用层和微服务层,实际上很少人能说清这两层分类的标准是什么?高内聚低耦合吗?就是这句看起来说了但又什么都没说的话在指导着架构设计。有没有更靠谱一点的方法论?这就是DDD想说的第一件事,它提供了一种给“应用层”或“服务层”分类的方法

接下来DDD想告诉你第二件事:保护好你的代码边界,否则它会变得腐化且难以维护。这句看起来理所当然的话事实上并没有那么容易做到,首先“边界”并不那么好界定,其次为了守住边界就得立一些规范,有时候为了赶项目进度,边界是可以模糊的,要知道一旦模糊了以后再想规范起来可就没那么容易了。

保护好边界后,面对边界内的核心代码,接下来就是DDD想说的第三件事:让模型回归业务的本质

以上就是DDD内核的全部内容,接下来我想调换一下顺序,先聊一聊看起来最抽象的第三件事。

问题空间和解空间

正式开始之前,我想先聊一聊问题空间和解空间。

问一个问题,把小球向斜上方30°以3m/s的速度扔出,小球多久能落地?小球落地时扔了多远?这是一道初中物理题,为了解这个问题,我们需要进行建模,忽略空气阻力的情况下小球会呈抛物线运动,结合三角函数和动力学方程,很容易能得出问题的解。(没算错的话大约是0.2s,扔的距离大约是0.4m)

可以看到问题空间到解空间的过程需要经过一些抽象,”抛物线“就是一个抽象,我们还要忽略空气阻力,暂不考虑扔球者的身高,合理的建模就能得到问题的解。然而解空间永远不可能等同于问题空间,只可能无限接近。比如你无法模拟出小球运行过程中的空气阻力,也不会为了算一道题就去测量常量g的数值,一般取9.8m/s²。

图片
图片

如果解空间的建模与问题空间相差较大(上右图),也就是说引入了问题空间以外的因素,例如抛球时人手心有没有冒汗,当天的空气湿度之类的。也许问题依然可解,但那会凭空增加许多复杂度。

让模型回归业务本质

在软件领域,问题空间就是业务规则,解空间就是你所建立的系统,你通过建立系统把这个世界的一部分规则通过数字化的方式运行起来。我们理想的方式是解空间是问题空间的一个子集,尽量不引入问题空间以外的因素,这就是让模型回归业务本质的含义。

怎样才能回归本质呢?DDD说要对领域进行建模,所谓的领域(Domain),大白话就是业务规则和业务概念,领域模型就是业务规则的集合。完整的领域模型应该是要包含服务(Service)、事件(Event)和实体(Entity)、DP(约等于ValueObject)等

  1. 服务说的是接口和流程
  2. 事件说的是状态变化或执行结果
  3. 实体和值对象是领域对象的载体

建模的过程大家应该不陌生,就是用UML工具把上面这些东西画出来,现在问题在于怎样让模型更接近业务的本质?DDD提供了一些方法和辅助工具:

  1. 让领域专家参与建模(对这个业务最了解的人)
  2. 建立统一语言(Ubiquitous Language),消除认知差异
  3. 用业务行为来描述接口,不要用技术概念。以账户为例,开户、销户、冻结、解冻、修改注册信息、修改支付限额等都是业务行为,增删改查就属于技术概念
  4. 使用充血模型
  5. 使用DP(Domain Primitive)而不是语言的基本类型。例如电话号码不要用String而用PhoneNumber
  6. 还有一些建模方法做为辅助:四色建模法、事件风暴等

这些方法有些听起来挺抽象,但只要回归到原点:让模型尽量还原业务,你就不会再纠结于这些概念。我们目标是建立一个能让人更易于理解的系统,我觉得评判标准也很简单,如果建立的模型在无需额外解释的情况下,技术能看懂、产品能看懂、业务专家也能看懂且各方达成一致,这个模型就是成功的。

保护代码的边界

上面说到业务模型要回归到业务本质,那么非业务的部分怎么处理?我们总要做分库分表、总要引入中间件,以及为了性能考虑经常需要做字段冗余。

这就是DDD强调的边界之一:技术代码和领域模型的边界。

这一边界的重要性不言而喻,我们一定不会希望有一天因技术架构的升级导致业务模型大改,在理想的情况下,业务模型和技术架构的演进是正交的。什么是正交?就像直角坐标系的x轴和y轴那样,分别朝不同的方向延伸,互不影响。

要做到这一点,我们就要合理地使用依赖倒置(Dependence Inversion),把技术细节封装在基础设施层(Infrastructure Layer)。例如数据存储就离不开对数据库的依赖,这时候如果让领域层直接依赖数据库层就会让数据库的技术特性(分库分表、事务、索引等)影响领域模型,这就导致了边界被打破,引入repository层就能解决这个问题:

图片
图片

我们的系统一定是由许多子系统或服务组成,子系统之间会有联系和依赖,有依赖就会有边界,这就是DDD说的边界之二:业务上下游的边界。

DDD为了更好地聊边界,直接引入一个新名词叫限界上下文(BC:Bounded Context),实在理解不了就把BC当成一个系统或服务就好了,BC之间的关系叫上下文映射(Context Mapping),上下文映射有很多种,篇幅原因就讲最常见的两种。

第一种:为了能更好地了解BC之间的协作关系,首先得区分上游(upstream)和下游(downstream),就像水流一样,上游的水脏了就会影响下游的水,怎样能降低影响呢?在中间建一个过滤网不就好了,这个过滤网就是防腐层(ACL:Anticorruption Layer)

图片
图片

注意区分上下游关系,受伤的总是下游,因此ACL也建在下游。通过ACL把上游传递过来的数据转化为下游可理解的模型,这样可以降低上游变化对下游核心模型产生直接的影响。

第二种比较常见的模式,两个BC之间有共享的部分,叫做共享内核(Shared Kernel)。这种情况下可以把共享内核抽出来单独做一个小型的BC,为啥要小型呢?因为这种模式存在一定风险,共享内核的修改会影响多个下游。

图片
图片

现在来想一想,你是否守住了自己的代码边界?可以评估一下,如果说下面这些事项都不需要侵入或改动你的核心模型就能实现,那就可以说代码边界是比较稳固的。

  1. 分库分表规则变化,原来分10张表已经不够用了,需要扩展到100张表
  2. 跑批框架需要从单机扩展到分布式
  3. 为了做高可用,数据通常分布在多中心,要做数据路由
  4. 上游说提供给你的字段规则有所调整,原来是取值0和1,要改为A和B

“分类”的哲学

现实里在程序员之间经常听到这样的争论:

  1. 我们的系统变得太复杂了,要拆分,应该拆成2个子系统还是3个子系统?
  2. 这个接口不应该写在我这里,应该在你那里

实际上这都是在说一件事,就是如何做系统和功能的划分。我认为架构设计到最后就是在做分类:怎么把内聚的功能划分在一起,同时又要考虑把风险隔离开。如果你要问一个架构师怎么做架构设计,也许他会嘿嘿一笑:“高内聚低耦合”。确实架构设计某种程度上是比较主观的,但也并不完全没有方法论可循,DDD在这方面就提供了一些参考。

- 从参与者出发思考

做过用例(Use Case)分析的都知道,参与者(Actor)是与系统交互的主体,参与者很大程度上决定了用例设计的方向,例如同样都是“查询交易记录”,从普通用户角度和从客服运营的角度出发去设计可能会差异巨大。因此按照参与者的不同来拆分可能会是个不错的选择,这样可以避免不同参与者的“利益”相互影响。

图片
图片

- 从模型角度去思考

我们在设计模型的时候,“类图”尤其好用,类图的元素比较全,既包含服务又包含事件和实体(如果你画的是完整类图的话)。从图上很容易能识别模型的请疏远近,这时候高内聚低耦合已经被具象化了,再去划分会变得so easy!

图片
图片

(随便画个示意图,大家能理解意思就行~)

- 从风险隔离角度去思考

在业务规模不大的情况下,或许这个因素会显得没那么重要。一旦业务规模上升到一定量级,“风险隔离”在架构设计中会越发占据更多的比重。

举一个电商业务的例子,在电商平台买东西的流程中,支付、扣减库存、生成订单这些环节应该都属于促成交易的“主链路”上,而生成物流单、支付成功后加积分这些环节就不在主链路,特别是在双11这样的高并发场景下,你一定不愿意看到因用户加积分这个功能挂掉导致用户整个交易失败。

因此在必要的时候,给每个功能标记一下风险级别,这也是一种分类的办法。

- 从组织架构思考

“康威定律”在IT界一定不陌生,这个定律揭示着组织架构与系统架构之间存在着微妙联系。我对这一条定律有深刻的体会,因为我曾在这样两种截然不同的组织架构下工作过

a. 在一级部门下先划分业务部门、产品部门、技术部门,技术部门下面再划分不通的业务方向。这种组织架构下技术部门有较高自治权,容易孵化出各种中台,技术选型和技术路线决策也会更加统一,人们会倾向于去思考“全局最优解”,但在实施业务战略的时候,需要更加费点劲才能形成组织合力

b. 先划分“事业线”,也就是业务方向,在事业线内部再划分业务部门、产品部门、技术部门。这种组织架构下每个业务各自为战,优先保证的是自己活下来,机动性强,但缺少全局统筹,技术架构通常是割裂的,一直在重复造轮子

不同的组织架构将会直接影响这个组织的思考和运作方式,在做架构设计的时候,技术架构不要试图去和组织架构做对抗,而是要协同。

我在阐述这一节的时候尽量避免用DDD本身的术语,毕竟我是希望透过现象看本质。但如果你看到这里已经对DDD产生了兴趣,不妨可以再查阅资料深入研究一下限界上下文(BC),上面所说的这些实际上都是在说BC,可以毫不夸张地说,BC是整个DDD最核心的一个概念(同时也最不好理解- -||)

总结

现在再来看这张DDD的典型架构图,你可能会更加理解DDD的思想内核,这是一种面向领域的设计方法,把领域层(业务规则)包在最里面,保护好边界,避免领域层被污染。DDD通过这样的方式降低建模和实现的复杂度。

图片
图片

一、构造块

构造块是指领域设计中最基本的一些元素。

图片
图片

构造块大图

1.1 分层架构

应用层和领域层的关系

  1. 接收订单
    • 顾客向前台经理(应用服务层)点单。
    • 前台经理验证订单的有效性。
  2. 协调厨房
    • 前台经理将订单传递给厨房(领域层)。
    • 厨房根据订单准备食材并烹饪菜品。
  3. 返回结果
    • 厨房将制作好的菜品交给前台经理。
    • 前台经理将菜品端给顾客。
规则
  • 前台经理不参与烹饪:前台经理只负责协调,不参与具体的菜品制作。
  • 厨房不直接与顾客交互:厨房只处理订单和食材,不直接与顾客打交道。
  • 不允许平级调用
    • 前台经理不能直接调用其他前台经理。
    • 厨房不能直接调用其他厨房。

5. 总结

  • 应用服务层 像餐厅的前台经理,负责协调和调度,不参与具体的业务逻辑实现。
  • 领域层 像餐厅的厨房,负责具体的业务逻辑实现,专注于领域模型和领域服务。
  • 两层的关系
    • 应用服务层调用领域层,完成业务逻辑。
    • 领域层处理具体的业务逻辑,返回结果给应用服务层

分层是常用的方法,不仅是领域驱动设计,计算机网络也会看到分层思想:网络的七层模型。分层的好处在于:可以集中精力关注每一层的功能职责,更好地分工协作。

分层的基本原则是:层中的任何元素都仅依赖于本层的其他元素或其下层的元素。下层如果不得已要调用上层的话,需要采用合适的模式,如观察者模式或者回调机制等。

常见分层如下:

  1. 用户界面层(或表示层):展示信息等。如:web 服务的 Contoller。
  2. 应用层:协调领域对象。如:支撑 Contoller 的 Service 服务。
  3. 领域层(或模型层):表达业务概念、业务状态信息、业务规则。
  4. 基础设施层:为上面各层提供通用的技术能力。如:数据库操作等。

用户界面层是客户端的统一入口,它只用于处理用户展示和用户请求,不会包含业务逻辑。但是,用户界面层也有它自己的逻辑,例如对用户的输入参数进行验证,但这种验证要跟领域层的验证逻辑区分开。比如对必传参数的空值校验,对数值类型参数的类型校验,这些都是用户界面层的职责;但如果要验证某个参数对应的业务数据是否存在,这种验证就应该是领域或业务逻辑的验证。在DDD编程模型中,这两种职责要区分开。

  • 用户界面层的请求和响应参数不在此层中定义,而是定义到了应用服务层,这样做的好处就是少了一次参数模型转换,应用服务层可以直接使用用户界面层的请求和响应参数(不建议把用户界面层的请求和响应模型定义到公共包中(如common包),因为这样做可能造成后期模型的滥用),弊端就是会在一定程度上破坏代码的封装性。
  • 用户界面层的响应实体要在此层中定义并转换,因为为了兼容多个用户界面层,应用服务层只会返回跟业务相关的领域模型,每个用户界面层对应的客户所需要的响应体可能是不同的,这时候每个用户界面层就需要把领域模型转换为不同客户方所需要的模型。(转换器最好也定义到应用服务层,如果定义到用户界面层,当不同客户端需要同一个参数时,在各个客户端对应用户界面层都需要定义相同的转换器)

应用服务层:为了减少模型转换带来的额外工作量,我们把用户界面层的请求和响应模型定义到了应用服务层(cn.huolala.demo.application.pojo),并把读写请求分离开,以便后期可以扩展CQRS架构模型。

  • 在应用服务层,除了必要的参数模型外,就是我们传统三层架构所熟知的service
  • 应用服务层不同于传统的业务逻辑层,它是非常轻量的,应用服务本身不处理业务逻辑,它是领域模型的直接客户,主要职责是用来协调领域模型做业务编排,此外,应用服务层也是表达用例和用户故事的主要手段。

领域层是整个架构的核心业务层,它不依赖其他任何的外部组件,领域层的职责就是通过领域实体和领域服务来聚合业务逻辑;领域服务并不是必须的,如果业务逻辑都可以用领域实体来完成,这个时候就不需要领域服务;那领域服务主要是做那些事情的呢?
  1. 不属于单个聚合根的业务,需要用领域服务去协调多个聚合完成业务逻辑
  2. 静态方法(在实践中,也可以为静态方法单独定义工具类,例如静态的验证器validator,静态的模型转换器convertor)
  3. 调用其他外部服务(如RPC)处理业务的,在拿到外部模型后,可能需要在领域服务中做业务校验或领域模型转换

  • cn.huolala.demo.domain.model包中定义了当前限界上下文所需要的所有领域模型(包括了非当前上下文),这些模型在DDD中也称为实体,它们都是充血模型,业务的聚合都由它们(cn.huolala.demo.domain.model.order.Order) + 领域服务(如cn.huolala.demo.domain.service.OrderDomainService)来实现。
  • 我们会在领域层中定义基础设施接口 (包cn.huolala.demo.domain.infra),以此来实现基础设施的依赖倒置。这里的做法是把基础设施做了进一步的拆分(例如资源库repository、远程服务调用remote、消息事件mq);还有一种比较好的实践:不对基础设施做进一步的区分,把所有的基础设施都视为资源,只定义资源库接口,这样的好处是在修改技术组件时,业务逻辑层可以做到0改动。

注:还有另外一种分包方式,就是把跟领域模型相关的所有基础设施放到一个包中,好处是可以使业务模型更加稳定,弊端是会破坏组件的封装性。

不管是数据持久化、缓存、事件、RPC、REST,只要是与数据和资源有关的内容,都可以是基础设施的范畴。基础设施层隐藏了技术实现细节,使我们更加关注业务代码,减少了技术实现细节对业务代码的侵入性。

  • 如果是倾向于架构模式清晰,可以为不同的资源获取方式定义不同的组件(本篇示例的做法);如果更加倾向业务模型的稳定性,则可以为所有的基础设施只定义一个组件层级(也就是只有一个infra模块)。
  • 举个例子:某项业务发展到一定规模后,需要对服务进行拆分,如果是传统分层架构模型,势必要在业务代码中修改或替换模型代码;如果我们使用了依赖倒置的六边形插件架构,我们就可以做到完全不修改业务代码:领域层定义的资源库接口不变,在基础设施层直接把原来数据库的实现改为远程调用的实现,业务代码0改动。之所以可以这么做,就是因为我们从架构层面限制了技术模型和业务模型的耦合关系,并对基础设施层应用了依赖倒置,领域层不会关心基础设施是由什么技术实现的,只需要给领域层所需要的领域模型就可以了。这正符合了整洁架构的依赖关系原则:外层的变更不应该影响内层的代码。
  • 示例中把数据库资源和远程服务资源分为两个基础设施组件还有一个特殊的原因:数据库资源在当前上下文是可以随意操作的,也就是增删改查,而且数据模型之间也会有复杂的关联关系,这种关系需要在领域层映射成领域实体的业务关系,所以领域模型和数据模型本质上就是不同的(也就是数据-对象的阻抗失调,如果使用JPA,则可以减小这种阻抗失调的影响);但是对于外部上下文的资源,我们多数是读操作,而且外部上下文返回的数据模型不会轻易修改,所以我们也可以把外部上下文的数据模型直接当做领域模型来使用,这样也避免了额外的一次模型转换。

DDD架构模型的分包规则符合最大复用最小闭包原则,各层模型分包独立,互不影响。

例如:作为用户界面层,controller和provider返回的模型可能会是不一致,所以我们把具体的返回模型包装在在各自的层级中,应用服务层只负责输出领域模型,由各个用户界面层决定把领域模型转换为各自返回给客户的模型。

1.2 关联

模型其实是真实世界模型的一个子集,不需要每个细节都反映出来,关联关系也是如此。

关联描述的是对象之间的联系,作者描述了3种减少关系复杂度的方法:

  1. 规定一个方向遍历:如,学校和校长是多对多的双向关系,但是如果可以考虑一个校长只在一个学校担任的职务的话,可以简化为学校到校长的1对多的关系。
  2. 添加一个限定符,以便有效地减少多重关联:如,在学校的某个特定时期,只有一位正校长。
  3. 消除不必要的关联:按需描述即可,less is more。
图片
图片

依赖关系示意

1.3 entity

实体 entity 的定义:主要由标识定义的对象被称做entity,特点是在整个生命中具有连续性。

简单理解为领域层中带有意义ID的对象,如:订单信息(含订单ID)。

1.4 value object

值对象 value object 的定义:用于描述领域的某个方面而本身没有概念标识的对象称为 value object。

简单理解为对象属性决定差异的个体,如:颜色RGB对象,只要rgb值一致,那么其实是一种颜色,并不关心对象的本身到底是哪个实例。

这类对象特点:

  • 经常作为参数在对象之间传递消息,常常是临时对象。
  • 设计选择:复制、共享或保持value object不变。
  • 为性能优化提供了更多选择:可缓存、可共享等。 实体 是由唯一标识定义的对象,具有生命周期连续性,通常代表业务领域中的核心概念。
  • 值对象 是通过属性值定义的对象,通常是不可变的,用于描述业务领域中的某个属性或状态。颜色地址
特点
  1. 无唯一标识:值对象没有唯一 ID,通过属性值来区分。
  2. 不可变性:值对象通常是不可变的,一旦创建,属性值不能改变。
  3. 临时性:值对象通常是临时对象,用于传递消息或描述某个状态。
  4. 业务描述:值对象通常用于描述业务领域中的某个属性或状态。
示例
  • 颜色:颜色由 RGB 值定义,只要 RGB 值相同,就是同一种颜色,不关心具体是哪个实例。
  • 地址:地址由省、市、街道等属性定义,只要属性值相同,就是同一个地址。
设计选择
  • 复制:值对象可以复制,因为它们的属性值决定了它们的唯一性。
  • 共享:值对象可以被共享,因为它们是不可变的。
  • 缓存:值对象可以被缓存,以提高性能。

1.5 service

领域层中服务 service 的特征:

  • 与领域概念相关的操作不是 entity 或 value object 的一个自然组成部分。
  • 接口是根据领域模型的其他元素定义的。
  • 操作是无状态的。

简单理解为领域层中一些需要协调多个对象的无状态函数。可以暴露领域中的一些功能。

值得注意的是:

  • service不只在领域层使用,根据职责,service也会分到各层中。
  • 可以控制领域层中接口的粒度。
  • 领域服务:封装领域逻辑,处理跨多个实体或值对象的操作。
  • 应用服务:协调领域服务和基础设施层,完成具体的业务用例。
  • 基础设施服务:提供技术实现,如数据库访问、消息队列等。

1.6 aggregate

聚合 aggregate 是一组相关对象的组合,有一个根(root)和一个边界(boundary)。外部对象只能引用根,而边界之间的内部对象之间则可以相互引用。

图片
图片

聚合根示意图

1.7 factory

工厂 factory 负责复杂对象和aggreate的创建,是领域设计的一部分。

1.8 repository

存储库 repository 为那些需要直接访问的对象提供数据访问能力,封装底层存储逻辑。

1.9 specifiction

声明 specifiction 是为特殊目的创建了谓词形式的 VALUE OBJECT,是一个谓词,可以用来确定对象是否满足某些标准。

场景:某些业务规则不适合作为Entity 或 VALUE OBJECT 的职责。

4. 总结

通过图书馆管理系统的比喻,可以更直观地理解声明的作用:

  1. 基本声明:用于判断单个条件(如年龄是否大于 12 岁)。
  2. 组合声明:用于判断多个条件的组合(如年龄是否大于 12 岁且没有逾期未还书籍)。
  3. 筛选对象:用于从集合中筛选出符合条件的对象。

代码语言:javascript
代码运行次数:0
运行
复制
class AgeSpecification {
    private int minAge;

    public AgeSpecification(int minAge) {
        this.minAge = minAge;
    }

    public boolean isSatisfiedBy(Reader reader) {
        return reader.getAge() >= minAge;
    }
}
使用

java

复制

代码语言:javascript
代码运行次数:0
运行
复制
Reader reader = new Reader("Alice", 14); // 读者 Alice,年龄 14
AgeSpecification spec = new AgeSpecification(12);

if (spec.isSatisfiedBy(reader)) {
    System.out.println("读者符合借阅条件");
} else {
    System.out.println("读者不符合借阅条件");

二、柔性设计

柔性设计(supple design):运用深层模型所蕴含的潜力来开发出清晰、灵活且健壮的实现,并得到预期的效果。

图片
图片

柔性设计关键要素

2.1 intention-revealing interfaces

释意接口 intention-revealing interfaces:在命名类和操作时要描述它们的效果和目的。

2.2 side-effect-free function

无副作用函数 side-effect-free function:尽可能把程序的逻辑放到函数中,函数不产生明显的副作用,把命令(引起明显状态改变的方法)隔离出来。

2.3 assertion

断言 assertion:对程序某个时刻正确状态的声明,在编程中编写 assertion 或者单元测试。

2.4 conceptual contour

概念轮廓 conceptual contour:领域本身的一致性,把设计元素分解为内聚的单元。

2.5 standalone class

孤立的类 standalone class:无需引用任何其他对象(系统的基本类和基础库除外)就能够理解和测试的类。把无关对象提取到对象之外,保持低耦合。

2.6 closure of operation

闭合操作 closure of operation:闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。在适当的情况下,可以定义操作的返回类型与参数类型相同。

三、战略设计

战略设计(strategic design):一种针对系统整体的建设和设计决策。

图片
图片

战略类型

3.1 bounded context

限界上下文 bounded context:特定模型的界限应用。限界上下文使团队知道什么必须保持一致,什么必须独立开发。

3.2 continuous integration

持续集成 continuous integration:把工作足够频繁地合并到一起,并使它们保持一致。

3.3 context map

上下文图 context map :项目所涉及的界限上下文以及它们与模型之间的关系的一种表示。描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。

图片
图片

上下文映射示意

3.4 shared kernel

共享内核 shared kernel :通常是共享核心领域或者是一组通用子领域。

图片
图片

共享内核

3.5 customer/supplier

客户/供应商关系 customer/supplier:上下游关系。不同客户需要协商来平衡,上游团队需要有自动测试套件。

图片
图片

客户/供应商关系

3.6 conformist

跟随者模式 conformist:单方面跟随模式。上游的设计质量较好,容易兼容,可以采用严格遵循上游团队的模型。

图片
图片

跟随者模式

3.7 anticorruption layer

防腐层 anticorruption layer:防腐层、隔离层,使用 facade or adapter 等模式。可以减少其它系统变动对本系统的影响。

图片
图片

防腐层

3.8 separate way

各行其道 separate way:声明一个与其它上下文毫无关联的 bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案。

图片
图片

各行其道

3.9 open host service

开放主机服务 open host service:开放子系统供其他系统访问。

图片
图片

开放主机服务

3.10 published language

共享语言 published language:把一个良好文档化、能够表达领域信息的共享语言作为公共的通信媒介,必要时在其它信息与该语言之间进行转换。

四、精炼

精炼(distillation):是把一堆混杂在一起的组件分开的过程,从中提取出最重要的内容,使得它更有价值。

图片
图片

精炼要素

4.1 core domain

核心领域 core domain:模型中最关键的部分,是程序的标志性部分,也是应用程序的核心诉求。

4.2 generic subdomain

通用子领域 generic subdomain:识别出对项目意图无关的内聚子领域,提取通用模型,并分离出来。

4.3 domain vision statement

领域愿景说明 domain vision statement:类似愿景说明文档,关注领域模型的本质,以及如何为企业带来价值。

4.4 highlight core

突出核心 highlight core:编写简洁文档描述 core domain 以及核心元素之间的主要交互过程,并把 core domain 标记出来。

4.5 cohesive mechanism

内聚机制 cohesive mechanism:把内聚的部分分离到一个单独的轻量级框架汇中, 并用释意接口暴露框架功能,从而使得领域元素关注“做什么”,“怎么做”转移给了框架。

4.6 segregated core

分离的核心 segregated core:把核心概念从支持性元素中分离出来,并增强核心的内聚性。把通用元素、支持性元素提取到其它对象中。

4.7 abstract core

抽象核心 abstract core:识别模型中最基本的概念(能表达出主要组件的大部分交互),并分离到抽象模型中(类、抽象类或接口)。抽象模型放在自己的模块中,实现类留在子领域定义的模块中。可以减少模块间依赖的复杂性。

图片
图片

抽象核心

五、大型结构

大型结构(large-scale structure):一组高层的概念、规则,它为整个系统建立了一种设计模式,能够从更大的角度来讨论和理解系统。

图片
图片

大型结构要素

5.1 evolving order

演变有序 evolving order:让概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。

图片
图片

演化示意

5.2 system metaphor

系统隐喻 system metaphor:当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。例如:用真实大楼的防火墙来类比网络的防火墙。

图片
图片

防火墙类比

5.3 responsibility layer

职责分层 responsibility layer:在具有自然层次的模型中,可以围绕主要职责进行概念上的分层,这样可以结合使用”分层“和”职责驱动的设计“这两个有力的原则。

图片
图片

地球的“分层”

5.4 knowledge level

知识级别 knowledge level:是一组描述了另一组对象应该有哪些行为的对象。当我们需要让用户对模型的一部分有所控制,而模型又必须满足更大的一组规则情况时,可以利用这个模式来处理。可以通过类型的知识,去选择不同的策略。

图片
图片

知识级别

5.5 pluggable component framework

可插入式组件框架 pluggable component framework:从接口和交互中提炼出一个抽象核心 abstract core,并创建一个框架,这个框架要允许这些接口的各种不同实现被自由替换

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • DDD的内核是什么
  • 问题空间和解空间
  • 让模型回归业务本质
  • 保护代码的边界
  • “分类”的哲学
  • 总结
    • 规则
  • 5. 总结
  • 用户界面层是客户端的统一入口,它只用于处理用户展示和用户请求,不会包含业务逻辑。但是,用户界面层也有它自己的逻辑,例如对用户的输入参数进行验证,但这种验证要跟领域层的验证逻辑区分开。比如对必传参数的空值校验,对数值类型参数的类型校验,这些都是用户界面层的职责;但如果要验证某个参数对应的业务数据是否存在,这种验证就应该是领域或业务逻辑的验证。在DDD编程模型中,这两种职责要区分开。
  • 应用服务层:为了减少模型转换带来的额外工作量,我们把用户界面层的请求和响应模型定义到了应用服务层(cn.huolala.demo.application.pojo),并把读写请求分离开,以便后期可以扩展CQRS架构模型。
  • 领域层是整个架构的核心业务层,它不依赖其他任何的外部组件,领域层的职责就是通过领域实体和领域服务来聚合业务逻辑;领域服务并不是必须的,如果业务逻辑都可以用领域实体来完成,这个时候就不需要领域服务;那领域服务主要是做那些事情的呢?
  • 不管是数据持久化、缓存、事件、RPC、REST,只要是与数据和资源有关的内容,都可以是基础设施的范畴。基础设施层隐藏了技术实现细节,使我们更加关注业务代码,减少了技术实现细节对业务代码的侵入性。
    • 特点
    • 示例
    • 设计选择
    • 4. 总结
      • 使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档