开发者社区 > 博文 > DDD落地实践-架构师眼中的餐厅
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

DDD落地实践-架构师眼中的餐厅

  • do****
  • 2023-12-30
  • IP归属:北京
  • 9520浏览

    本文以餐厅场景为叙事主线,以领域驱动为核心思想,结合架构设计与功能设计方法论,从领域分析到落地的全过程案例,内容偏重于落地,因此不乏一些探讨,欢迎指正。

    全程干货、耐心读完、必有收获。


    1、领域设计

    让我们抛开技术人员的本能技术视角、站在纯业务视角来分析领域问题

    领域设计的核心是分而治之,目的是实现业务领域的自治性

    就像你平时不会将枕头和被子放在厨房或卫生间一样,你的床上不会放着大米白面,否则你想睡觉是一件很复杂的事情,软件系统也是如此,这就是我们要解决的问题。

    我们的核心问题是:“谁”通过什么“行为”影响了“谁”,其中的三个要素分别是:角色、行为、实体

    角色:是施事主语、是名词,是主动发起行为的一类实体。

    行为:是动词、是做了什么事情,是行为本身。

    实体:是名词,是除“角色”之外的其他实体。


    1.1 宏观流程

    假如我要设计一个餐厅,我会首先从宏观流程去分析,可以帮我们迅速找到重要的行为。

    因此会得到几个明确的行为区域,我将餐厅划分为“菜品域”,“订单域”,“厨房域”,“用餐域”,这仅仅是一个初步的划分,后续再进入细节。

    产出物是:宏观流程和参与角色


    1.2 统一语言

    语言贯穿于整个开发过程,从需求分析到设计、从设计到编码,因此好的语言非常重要,好的语言体现了清晰的业务概念

    在这个阶段,我们需要通过梳理,找到业务中都有哪些实体与行为,对其做一些归纳。我的建议是先找到 “实体”、“角色”、“行为”,并对其归类,我常常关注角色以及具体身份、实体以及实体实例,功能以及包含的重要步骤。

    推荐使用脑图画出来,我认为归纳后的脑图有助于我们识别根本要素。

    产出物是:名词、概念定义、相关脑图。


    1.3 用例分析

    在上阶段,我们找到了角色和行为,但并没有明确他们之间的关系,所以我们通过用例分析的办法,梳理角色与行为之间的关系

    产出物:用例图,体现角色与用例之间的关系

    以做菜为例,如图


    1.4 领域划分

    我们在分析宏观流程时,划分了几个行为区域,但那是比较粗糙的。基于之前的用例分析,我们可以更进一步,按照“功能相关性”、“角色相关性”处理领域的划分

    功能相关性:任何业务都是由一套用例组成的,业务的某个领域也不例外,所以领域的划分应该以功能相关性为主,例如与做菜相关的用例都应该归属于厨房,所以我们确认了厨房域,这是很自然的事。

    角色相关性:其次是角色,常用于划分子域,某个区域涉及多个角色参与,可以按照角色的分工,拆分为多个子域,从而满足不同角色的个性化需要。例如厨房的采购人员负责买菜、刀工负责切菜、大厨负责烹饪。我们就会考虑将厨房划分为“采购域”、“加工域”、“烹饪域”。

    通常来说,子域不具备独立的问题空间,不会作为独立的领域存在。

    产出物:领域、子域

    以厨房域为例,如图



    1.5 领域建模

    这是大家比较熟知的阶段,重点分析实体与领域之间关系(领域聚合),实体与实体的关系(OO聚合)

    领域模型是实现功能的基石、需要有对功能的本质理解,才能找到最核心的实体,实体之间的OO聚合关系决定了功能的扩展性,OO聚合是最重要的核心点。

    有时候领域模型的名字与领域的名字是一样的,但他们的含义不一样,领域是区域,领域内的模型是区域内的实体。


    组合、聚合

    聚合(aggregation):聚合关系是一种弱的关系,整体和部分可以相互独立。

    组合(composition):组合关系是一种强的整体和部分的关系,整体和部分具有相同的生命周期。

    可以使用如下案例,既能表达领域聚合,又能表达OO聚合的关系。

    产出物:聚合、实体、值对象、实体的属性

    (领域服务和事件在功能设计中提供,这里无法提供)

    1.6 领域上下游

    领域上下游关系,不是领域的依赖关系,依赖关系指的是能力的依赖,是共用了某些能力,依赖关系是固定的。领域上下游关系,也不是调用关系,调用关系是与用例相关的,并非描述领域处境的。

    领域上下游关系指的是影响力的关系,上游影响下游,影响力分为“逻辑影响”和“数据影响”,一般说来我们更应该关注“数据影响”,所以领域上下游关系是一种数据流向的限定,是业务发生的顺序限定,用于规定该领域所使用的数据,是下游领域依赖上游领域“准备就绪”的体现。合理的上下游限定,有助于减少领域之间的不必要依赖,有利于数据的复用并减少重复计算。

    例如,厨师不依赖采购人员的采购功能,也不依赖刀工的切菜功能,他只是依赖“初加工食材”而已,而食材就是被处理好的数据,厨师在做饭时,菜就已经被切好了。

    领域上下游是与场景相关的,并不是一成不变的,不同的场景存在不同的上下游,各场景应该独立说明。

    产出物:各场景的上下游说明


    例:在【菜品管理】场景下

    如果厨房的某些食材不足了,或者某个厨师休假了,就会影响到菜品的展示,从而影响到客户的订单。


    例:在【客户消费】场景下

    客户的订单、影响厨房生产的菜,从而影响刀工的行为,也影响到了采购。


    请对比下面两个图,用于理解领域的上下游


    2、架构设计

    架构设计是为了解决软件系统复杂度带来的问题,找到系统中的元素并搞清楚他们之间关系

    架构的目标是用于管理复杂性、易变性和不确定性,以确保在长期的系统演化过程中,一部分架构的变化不会对其它部分产生不必要的负面影响。这样做可以确保业务和研发效率的敏捷,让应用的易变部分能够频繁地变化,对应用的其它部分的影响尽可能地小。

    架构设计三原则:合适原则、简单原则、演化原则

    2.1 分层架构

    我们需要按照 接口层、领域(用例层、模型层、依赖层)、基础层 构建架构模型

    接口层:为外部提供服务、是流程编排层,是门面服务,是下游领域能力的整合层。不实现任何功能逻辑,也不处理事务。是横跨领域的,是应用或系统的一个入口层。

    领域用例层(领域服务层):顾名思义,是领域用例的实现层、隶属于某个领域、是业务逻辑层,是事务层,业务逻辑应该在这层完整体现,不要分散到其他层级,不要实现与领域不符的用例。

    领域模型层:是领域模型(实体、值对象、聚合)的所在位置,专注于领域模型自身的能力,不包含业务功能,可以处理事务,是相对原子化的能力,是领域对象的自我实现。推荐使用充血模式。

    领域依赖层: 包括仓储,端点、RPC等,主要就是领域和外部解耦,保持领域是独立的。

    基础层:与业务无关的、通用的技术能力,技术组件等,可有可无。


    2.2 架构映射

    架构的视角,从大到小依次是:系统->应用(微服务)->模块(包)->子模块 这样的从大到小的层级。

    业务领域映射:我们将划分好的领域,按照对应的视角映射为对应的元素,领域模型映射到架构模型时,应该是视角对等的,如果餐厅是系统、那么厨房就是应用,如果餐厅是应用、那么厨房就是模块。也应该层级匹配的,将用例的实现映射到用例层,将领域模型的实现映射到领域模型层。

    技术和抽象问题:有时候、业务领域分析不能体现那些共性的技术问题,所以需要适当结合技术视角,可能需要对领域模型微调。同时、我们需要找到共同需要的基础能力,例如“水”、“电”、“煤气”、“放水”等等,将这些作为额外的考虑因素,要做到业务问题与技术问题解耦,不要将技术问题和业务逻辑揉成一团

    领域设计,类似餐厅设计师,他设计餐厅有几个区域,区域的用途是什么。

    架构设计,类似建筑设计师,他设计如何走水电煤气、如何施工等。

    产出物:分层架构图


    以厨房为视角,其架构如下


    以餐厅为视角,其架构如下

    分层架构图,体现逻辑上的层级分布,而不是代表组件的具体含义,组件是应用还是模块、需要结合实际情况而定。


    2.3 必要的约束

    1、分层架构越往下层就越是稳定的:下层是被上层依赖的,下层不可以反向依赖上层(扩展点除外)。因为分层架构的核心原则是将容易变化的逻辑上浮,将共性的、原子化的、通用的逻辑下沉,被依赖者应该是稳定的,地基的稳定才能保证上层的稳定,这要求上层背负更多业务逻辑。

    2、在使用充血模型时,应该符合面向对象编程原则:不要随意的将一些能力都充到领域实体模型中。以“菜”为例,重量和规格是“菜”的自身的属性,激发味蕾是“菜”的能力,“菜”可以维护自身的持久化状态。但是、请注意、“菜”不可以“炒菜”,因为“炒菜”的时候,“菜”还没有出现呢,“菜”不是自己的上帝,“菜”需要被做出来,所以“菜”被做出来之前是没有“菜”的,这是个时间上的概念,不要错把“炒菜”的能力放在“菜”的身上。“炒菜”用到的“水+电+气+食材+调料+厨具”不应该是“菜”的属性范围,这些元素都在“厨房”的范围中,不要让领域的模型包含不属于自身的元素、领域的实体模型只是领域的一部分

    3、接口层是与应用相关的,可以依赖各领域的所有功能:接口层不属于任何领域,领域也不一定要有接口层,所以接口层不会被限制在某个领域,也是更高的独立层级,他可以直接调用想要使用的功能。

    4、领域三层(用例层、模型层、依赖层)结构与环境无关:分层架构是逻辑分层的体现,无论某个领域是应用还是模块,都应该具备这三层,无论应用中有几个领域,你都不可以用接口层替代领域用例层,无论你调用应用内的其他领域 或 应用外的其他领域,都应该通过本领域的依赖层调用。即使多个领域在同一个应用当中,也要按照他们是分别独立的应用去看待,该有的都要有。

    5、领域应该是最小完备的:把一个领域拆分为子域、子子域、子子子,,,无限拆分,拆分到一定程度之后,某个子域就不完整了,不完整的子域是不可以独立存在的。拆分不不够或者过度拆分,都是不符合低耦合高内聚原则的。我们必须清晰的指出哪个领域是独立存在的领域,哪个领域是子域。这就是领域和子域的区别,子域只是用于区分边界,子域不具备独立存在性,子域不需要独立拥有领域三层,所以同一个领域的子域之间可以直接调用,不必通过领域依赖层调用,也不必严格解耦

    6、领域服务层就是领域用例层:他们俩是同一回事儿,都是用于实现领域内的用例的。不要将他们视为两个独立的层,否则会导致业务逻辑分散。更不要将领域服务和领域模型混为一谈,错误的将业务逻辑写在领域模型中,这会导致业务逻辑进一步下沉。业务逻辑的不确定性太大,是不适合下沉的,是违反分层架构原则的,重点就是不要让功能逻辑分布多个层中


    2.4 微服务划分

    服务划分以领域划分为参考,主要看我们要拆分到什么粒度,这 应该符合低耦合高内聚原则,不破坏领域实体的聚合关系

    产出物:微服务


    例如餐厅:餐厅是有必要拆分的,餐厅的“菜品域”,“订单域”,“厨房域”有独立的问题空间。

    例如厨房:厨房是没有必要拆分的,厨师与刀工的耦合非常高,他们都在做饭,分开之后是不完整的,分开就是没有必要的。

    餐厅被拆分为:厨房(Kitchen)、菜品(Category)、订单(Order)三个微服务,当然还有两个非领域的服务:餐厅门面服务、餐厅基础服务

    3、功能设计(用例实现)

    如果说领域设计是餐厅的设计师、架构设计是餐厅的建筑师、那么功能设计就是餐厅的厨师或服务员。

    任何设计都要落地到功能设计,如果厨师不守规则,偏偏要去洗手间洗菜,最后的结果依然是一团乱,最终会导致设计无法落地。


    功能设计是实现 “面向扩展开放、面向修改关闭” 的途径,是指导研发落地必备环节。


    3.1 用例的位置

    我们在领域分析章节,已明确了用例与角色的关系,用例与领域的关系

    然而一个新功能的加入,我们仍然要再次评估,以确保他处于正确的位置。

    产出物:更新用例图

    3.2 功能的概念

    功能迭代时,功能会发生一些变化,所以他的含义是可能变化的,所以我们需要再次审视功能的概念,及时加以调整。

    例如、我们实现了一个“做蛋炒饭”的功能,后来又实现了一个“做辣椒炒蛋”的功能,那么我们应该将功能升级为“炒菜”,甚至是“制作菜品”等。

    明确功能的概念,是功能设计的前提。

    产出物:更新语言库,更新脑图


    3.3 用例分析

    这里的用例分析不同于之前的用例分析,这里的用例分析需要深入细节,我们需要将功能拆分为多个子功能(步骤),并确认每个步骤的含义,关注共性和差异。

    1、确认用例的泛化,找到差异点,实现功能的扩展。

    2、寻找共同包含的功能,找到共同的逻辑,实现逻辑的复用。

    产出物:用例分析图

    例:做饭(做大拌菜、做铁锅炖、做炒鸡蛋、做蒸米饭、做炒米饭)


    3.4 用例实现类(领域服务类)结构图

    结构决定扩展,要专注于用例层的类设计,用于解决用例的共性和差异问题,实现“面相修改关闭,面相扩展开放”,用例的类结构图是用例分析图的一种映射

    出物:用例层的类结构图

    不一定非要用模板模式,根据情况而定。


    3.5 用例流程图

    我们搞清楚类图之后,就该把对应的步骤分配到各类当中,这将是类中的方法,进一步明确由哪个类和方法来实现该步骤,推荐使用泳道图表达上述内容。

    步骤也是要有明确的业务含义的,要符合单一职责原则的。

    泳道的纵向组件是用例的实现类,我们不掺杂任何其他组件,专注于表达用例类之间的协作,用例中所有的步骤都应该得到体现,这是真实业务流程的映射

    产出物:用例流程图


    以炒鸡蛋为例,其用例流程图如下


    试想一下、把功能逻辑放在领域模型当中(例如在聚合中),那如何实现“面相扩展开放、面相修改关闭”呢?显然是很难的,他们面面对的不是同一类问题,需要用不同的技术模型。

    3.6 活动图(时序图)

    这是最后一步了,我们使用时序图,确认每个步骤的依赖细节,体现调用关系。

    我们将 用例层 与 领域层 与 依赖层 结合起来,明确用例的每个步骤由哪个领域服务提供支持。进一步说明用例是如何实现的。

    这时候,为了简便、我们可以收起用例类的泳道。

    产出物:时序图、活动图



    4、编码实现

    编码实现......  我决定还是......  偷个懒吧......  哈哈哈。

    但是我们回顾一下之前的内容,是否足够指导编码了?

    不同的研发人员依照设计去编码,是否会写出不一样的代码? 如果是的话,说明设计还要再完善,如果否的话,至少说明设计已经完整。


    系统名

    餐厅系统

    相关应用

    厨房应用、菜品应用、订单应用,门面应用、基础应用

    系统架构图

    厨房应用-领域模型

    厨房应用-用例层

    厨师模块、刀工模块、买菜员模块

    厨房应用-用例层-厨师模块(服务类结构)

    厨房应用-用例层-厨师模块(类中的方法、方法含义、执行流程)

    厨房应用-用例层-厨师模块(方法的依赖、调用关系)


    最后、我们的目的是:“解决软件复杂度带来的问题”。









    文章数
    3
    阅读量
    1684

    作者其他文章

    01 缓存空间优化实践