软件设计:领域驱动设计

软件设计:领域驱动设计

1、领域驱动设计的概念

领域驱动设计(Domain-Driven Design,简称DDD)是一种以业务领域为核心的软件设计方法。其核心思想是通过构建领域模型,将业务逻辑与技术实现紧密结合,从而解决复杂业务系统中的设计和演进问题。

因此,领域驱动设计的核心是:

  • 聚焦业务领域‌: 强调将业务规则、流程和概念直接映射到软件模型中,确保软件结构与业务逻辑深度耦合。

  • 统一语言(Ubiquitous Language)‌: 开发团队与业务专家使用一致的术语描述业务逻辑,减少沟通歧义。

  • 分层架构‌: 通过战略设计划分清晰的业务边界(限界上下文),避免模型污染;通过战术设计实现领域模型的技术落地。

领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质,并且与任何技术实现无关‌。

所谓的「领域」,是对特定业务的描述,比如汽车是一个领域,衣服是一个领域,家居是一个领域,外卖也是一个领域等等。通常由该业务的垂直协作方共同确定,比如产品需求、系统架构、程序代码,由一群“专业的”人承接,这意味着其中的每一个人,可能都是该「领域」内的专家,而「领域模型」成了他们之间的「通用语言」,或者说,「领域知识」让彼此能够坐在一起讨论问题,再换句话说,产品也可以使用此通用语言来“组织代码”。

在这里插入图片描述

领域知识让在同一个领域的掌握不同类型知识的用户、业务人员、开发人员可以坐在一起讨论问题;

领域模型是用户、业务人员、开发人员等不同人员交流的“通用语言”,让问题讨论更高效。

领域驱动设计(DDD,Domain-Driven Design),它包括一组实现模式(一般称为“战术模式”)和一组架构模式(一般称为“战略模式”)。其中,战术模式从技术层面实现领域模型,包括实体、值对象、领域服务、领域事件、聚合、资源库、工厂和分层架构;战略模式从宏观层面划分领域,包括子域、限界上下文和上下文映射。

2、领域驱动设计:战略模式

1、战略模式:子域

一个领域,由一个或多个子域构成。

比如研究汽车领域,可以划分为轿车子域、SUV子域、公交车子域等

2、战略模式:限界上下文

限界上下文可以用细胞来做比喻,细胞功能完备,自成一个系统,最关键的是有一层细胞膜,能把细胞和外部世界隔离开来。同理,限界上下文本质上是一个自治的小世界,它有完备的职责,还有清晰的边界。

限界上下文的最理想边界是子域的边界。

一个子域的一切资产,包括领域模型、数据库、包、可执行程序、接口声明等,都应该封装在限界上下文中,避免跨越边界。

例如,如果你在一个业务中需要发送短信,那么大概率你不会自己实现这个功能,而是会直接找一个可以发送短信的服务提供商,使用公开的API或者SDK完成。这个短信服务就包裹在一个限界上下文中,对你来说,仅需要了解它的公开接口。

3、战略模式:上下文映射

领域驱动设计通过上下文映射(Context Map) 来讨论限界上下文之间的协作问题。

每个限界上下文都不是独立存在的,多数时候,都需要多个限界上下文通力协作,才能完成一个完整的用例场景。

在这里插入图片描述

如上图所示:要实现下单之后的配送这一场景,则需要订单上下文和图计算上下文协作(通过【配送路径计算适配器】协作)才能完成。

3、领域驱动设计:战术模式

1、战术模式:实体、值对象、领域服务、领域事件

在这里插入图片描述

如上所示,在外卖这个领域:

领域服务:只有在确实表达了一个相对独立的业务概念或者业务策略,并且不能简单地把它归结到某个既有的业务对象上时,才是一个真正的领域服务。比如取餐点推荐就是领域服务。

领域事件:领域事件代表从业务专家视角看到的某种重要的事情发生了。例如,当用户提交一个订单时,会产生一个值得关注的业务结果,即订单已提交;当用户完成支付时,会产生一个事件,即已支付。领域事件指的是业务专家关心的事件,它源自于业务活动的结果。

实体:是一类重要的业务对象。实体需要用ID作为唯一标识,这是它的关键。

值对象:是描述性对象,描述的是具体内容。值对象,我们只关心它的属性,并不关心它有没有唯一标识。

2、战术模式:聚合

在这里插入图片描述

用边界把那些紧密相关的对象放到了一起。

处在同一个边界内的对象就形成了一个聚合。

聚合是业务完整性的基本单元。

如上图所示,处于虚线内的就是一个聚合。上面的虚线表示了完整的订单信息,下面的虚线表示了完整、独立的支付订单。

上图的聚合可以用下面的代码来表示:

public class Order {
    Id id;
    SubmitterId submitterId;
    List<OrderItem> orderItems;
    int totalPriceInCent;
    OrderStatus orderStatus;
    // 方法部分略
}
public class OrderItem {
    FoodData food;
    int quantity;
    int unitPriceInCent;
    int totalPriceInCent;
    // 方法部分略
}
public class FoodData {
    FoodId id;
    int unitPriceInCent;
    String description;
}

3、战术模式:资源库

对于查询、创建、修改、删除数据的操作,领域模型使用“资源库(Repository)”这个概念来承载它们。下面是一个简单的具体实现的例子:

public interface OrderRepository {
    Order findOne(OrderId id);
    Page<Order> findAll(Pageable pageable);
    void save(Order order);
}

一个聚合对应一个资源库。可以把资源库声明为接口。

4、战术模式:工厂、四层架构

领域驱动设计中的工厂和《设计模式》中的工厂本质上是相同的,都是分离了构造和使用,并且封装了对复杂对象的构造过程。不同的是,前者更强调聚合的关注点,保证了聚合的业务完整性。

在这里插入图片描述

传统三层架构为:表现层、业务逻辑层、数据访问层

四层架构:表现层、应用层、领域层、基础设施层。

其中四层架构中的应用层、领域层是传统三层架构业务逻辑层的拆分。

上面的四层架构,可以翻译为下面的代码:

── main
    └── java
       └── com.project.sample
          ├── interfaces // 表现层
          ├── application // 应用层
          ├── domain // 领域层
          └── infrastructure // 基础设施层

4、领域驱动设计示例

1、后端领域驱动设计工程示例

 ── main
   └── java
      └── example.food
      ├── interfaces
      ├── application
      ├── domain
      │  ├── services
      │  │  └── PickupSiteRecommender // 取餐点推荐策略
      │  ├── order
      │  │  ├── Order                 // 订单聚合根
      │  │  ├── OrderItem             // 订单项值对象
      │  │  ├── OrderFactory          // 工厂
      │  │  ├── OrderRepository       // 资源库
      │  │  ├── OrderStatus           // 值对象
      │  │  └── OrderCreatedEvent     // 领域事件
      │  └── food
      └── infrastructure

2、前端领域驱动设计工程示例

对于业务系统的开发,通常我们习惯以页面维度组织代码,将页面里的组件进行拆分,这样开发起来很直接,但是会引发如下问题:
1、Component1 一开始被划分到Page1,但是后来发现 Page2 也需要,Component1 继续划分在 Page 就不合理了,可能需要重新进行目录划分,或者组件的提取。
2、PageX 是⼀个新的页面,新的页面也会用到 Component1,但是开发者并未参与过 Page1 和 Page2 的开发,开发者对 Component1 的复用变得不可控。

如果仓库是多版本,比如存在主站版本和商业化版本,版本之间的核心功能基本是⼀致的,但是一定会存在差异,一方面要对相同功能代码的同步,一方面⼜要保持彼此之间的差异,以上的一系列问题,使得这种场景变得很困难。

透过现象看本质,以上问题其实很⼤程度都跟“边界”有关,前端代码的边界确实让⼈难以把控,而领域驱动时分擅长解决“边界”的问题。

比如要进行图研发平台的开发,可以通过领域划分得到图研发平台的领域模型:

在这里插入图片描述

通过上面的领域模型,可以进行代码工程的目录结构的对应构建:

在这里插入图片描述

components 为公共组件,可以被所有聚合引⼊,pages为页面组件,domains 即代表⼦域,如 domains-core 代表核心域,核心域下 graph-project、graoh-config 等为界限上下文,代表图项目、图配置等,graph-project 下的 project-info、project-list 等则对应聚合,聚合下的内容:

在这里插入图片描述

① components

该聚合下的组件,该目录下的组件,除了公共只能引⼊该聚合目录下的内容,也就是说组件在聚合内是自闭环的。

② entities

entities下可以定义多个实体,每个实体内都声明了该实体的属性和方法,如 project-info.ts 我们看到ProjectInfoEntity 里定义了该实体的属性,以及获取属性的方法和更新属性的方法,为了保证代码语义updateProjectInfoEntity 方法是不允许暴露出去的。

entity 所定义的属性,是在视图层直接进行消费的,不需要做数据转换的。

③constants

该聚合下用到的常量,这里可以统一书写规范,例如只能大写字母加下划线。

④ services

该聚合下⽤到的后端接口服务,定义为⼀个类,如:service类起到如下作用:

1、声明了该聚合下用到哪些服务,这些接口服务的入参和出参都是明确的,将来如果涉及到接口变更或替换,可以直接在这里做变更,尽可能减少视图层的变更。

2、规范后端接口的命名。

⑤ translator

上面提到,我们要求 entity 的数据是视图层直接消费的,后端的数据很多情况下是要做转换的,这就需要 translator,将后端接口数据转换为视图层可以直接消费的数据。

⑥ transformer

还有⼀类数据是前端提交到后端的,典型的如表单场景,可能也会涉及到数据的转换,将前端提交的数据转换成后端接口接收的数据。

在这里插入图片描述

整体开发流程:

在这里插入图片描述

最后,笔者想要强调的是,领域驱动设计是一种思想,但无论什么设计思想,软件架构,都没有谁是最好的,只有适合当前团队的才是最好的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

碳学长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值