领域驱动设计-简介

简介

领域驱动设计:Domain Driven Design,DDD,一种软件设计的方法论,围绕业务构建领域模型,通过领域模型来设计软件,以此来控制软件的复杂性,解决软件难以理解、难以演进的问题。它提出了一系列的概念和模式,以帮助开发者更好地建模和实现业务逻辑。

使用领域模型来设计代码

这里先学习根据领域模型设计出来的代码大概是什么样子的,相较于其它软件架构有什么不同或好处。

领域模型中的分层架构

领域模型将软件系统分为四层:用户接口层、应用层、领域层、基础层。

  • interfaces 用户接口层:负责和用户交互,通常会把参数校验、统一异常处理等功能,放在用户接口层
  • application 应用层:流程编排,调用领域层、基础服务层的服务来实现功能,还有安全认证、事务控制等系统级服务。
  • domain 领域层:核心业务代码,处理业务逻辑,这一层必须是最稳定的,除非业务有大的变动
  • infrastructure 基础层:包括数据库访问、缓存、外部依赖、工具类等,为所有层提供服务

相较于传统的三层架构,controller、service、repository,领域模型中的分层架构,最主要的特点就是将原先属于服务层的代码分散到了应用层和领域层。因为领域驱动设计,会根据业务来设计领域模型和领域服务,这是系统最核心的业务逻辑,这些逻辑会被放在领域层,然后在应用层,通过把多个领域服务组合在一起的方式,来实现具体的业务功能。领域会被分的很详细,单个领域服务通常无法实现业务逻辑,应用层也相当于领域层的适配器,它会对领域层屏蔽外部变动,保证核心业务的稳定。

严格分层架构和非严格分层架构:

  • 严格分层架构,任何层只能对位于其直接下方的层产生依赖
  • 非严格分层架构,和严格分层架构正好相反

使用DDD编写代码的基本步骤

代码设计:

  • 根据业务,来划分领域、边界,边界又称限界上下文,
  • 在领域中,设计领域模型,领域模型的基本单位是聚合,聚合中包括实体、值对象、聚合根。
    • 实体:实体就是代码中的类,这里的实体用来表达业务模型
    • 值对象:值对象是实体中的一个属性集合,值对象是不可变的。实体由值对象和普通属性组成。
    • 聚合和聚合根:多个业务上紧密关联、需要同步变化的实体,被逻辑上划分到一个聚合中,然后用一个聚合根来代表这个聚合,聚合根是一个特殊的实体类,通过聚合根来操作这个聚合。

代码实现:

  • 限界上下文通常是微服务的边界,限界上下文中的一个领域就是一个微服务。
  • 在一个微服务中,依据代码分层架构,将领域相关的代码放到领域层,在领域层中,会根据逻辑上的聚合,来组织代码,一个聚合相关的实体类和服务会被放到一个包下,并且聚合之间是松耦合的,将来如果微服务要继续拆分,就是以聚合为单位进行拆分。

使用DDD设计代码,先写好的是领域层,实现核心业务逻辑,然后再设计数据库,决定领域层的实体要怎么存放在数据库中,最后设计用户层、应用接口层,将前端请求转换为领域模型,调用领域模型的代码,实现业务功能。

DD中的实体类类型:和三层架构的实体类类型基本相同,主要是对于DO的概念不一致。

  • PO:Persistent Object,数据持久化对象,与数据库结构一一映射,是数据持久化过程中的数据载体。
  • DO:Domain Object,领域对象,微服务运行时的实体,是核心业务的载体。DO有时候又叫做Data Object,为了避免混淆,建议领域模型不要加DO后缀
  • DTO:Data Transfer Object,数据传输对象,用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。
  • VO:View Object,视图对象,用于封装展示层指定页面或组件的数据。

实体类的模型转换发生在什么位置:

  • 前端传给用户的是dto
  • 应用层:将dto转换为领域模型,然后调用领域模型的代码
  • 领域层:不做模型转换
  • 基础服务层:接收领域模型作为参数,内部使用数据模型或其它,取决于请求数据库还是请求rpc服务,请求数据库使用数据模型,请求rpc服务使用dto。

领域层不做模型转换,由其它层来做,所以其它层相当于领域层的适配器。

核心概念

领域:Domain,业务的活动范围,系统所要解决的问题的业务背景。复杂的业务中,还可以细分为子域、核心域、通用域、支持域。

  • 子域:sub domain,领域的细分
  • 核心域:core domain,领域中最重要的部分,是系统的核心竞争力所在
  • 通用域:generic domain,领域中通用的、非核心的部分,可以在多个系统中复用
  • 支撑域:supporting domain,用于支撑核心域

限界上下文:Bounded Context,一个明确的边界,定义了某个子域的模型及其责任范围,每个上下文内的模型和逻辑都是自洽的。领域边界就是通过限界上下文来定义的。限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务

聚合:aggregate,是一个或多个对象的集合、是一个边界,聚合是逻辑上的。聚合中的对象在业务上紧密相关,并且需要在业务操作中保持一致性。聚合是领域模型的基本单位。

  • 聚合根:aggregate root,一个特殊实体,它是聚合的入口点,外部系统只能通过聚合根来访问和操作聚合内的其它对象。
  • 实体:entity,具有唯一标识的对象,其生命周期和属性可能发生变化
  • 值对象:value object,没有唯一标识的对象,描述领域中的某些属性。值对象是不可变的,其属性一旦设置就不可改变

领域

领域就是业务背景,限界上下文就是对领域进行划分,划分成不同的小领域,也就是子域,直到子域不可以再分。

领域模型

实体

实体:对应业务对象,它具有业务属性和业务行为,实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。实体是最基础的领域对象,它的行为表现出的是个体的能力。

贫血模式和充血模式:

  • 贫血模式:实体类中没有业务逻辑,业务逻辑放在服务类中
  • 充血模式:实体类中有业务逻辑,把逻辑上只属于当前实体类的业务逻辑放到当前实体类中

值对象

值对象:主要是属性集合,对实体的状态和特征进行描述,值对象是不可变的。

值对象的作用:

  • 保证属性归类的清晰和概念的完整性,避免属性零碎。
  • 在领域建模时,可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;
  • 在数据建模时,可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

聚合

聚合:由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,是领域模型的基本单位。

聚合的作用:

  • 通过将相关对象组织成聚合,可以简化复杂系统中的数据管理和业务逻辑实现。
  • 微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照DDD方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,就不需要花费太多的时间和精力了。

聚合根

聚合根:聚合中的一个特殊实体,它是聚合的入口点和唯一暴露给外部的接口。外部系统只能通过聚合根来访问和操作聚合内的其他对象。

判断一个实体是否是聚合根的方法:是否有独立的生命周期?是否有全局唯一ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体

聚合间的引用:通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。

子聚合操作:在某些情况下,可以允许对子聚合的直接操作,前提是这些操作不会影响到聚合根的业务逻辑和一致性。如果发现频繁需要直接操作子聚合,可能需要重新评估聚合边界,考虑是否需要调整聚合设计。确保在直接操作子聚合时,仍然能通过某种机制来验证业务规则。

领域服务

一个聚合通常对应一个领域服务,领域服务用于处理不属于单个实体的业务逻辑

基础服务

实现了数据库访问、缓存、rpc等操作。有一点值得注意,DDD规定把数据库访问的接口定义在领域层,不过在实践中,数据库访问通常是定义在基础服务层

应用服务

位于应用层,应用服务用来处理跨多个聚合的逻辑

案例

案例:订单系统中订单的领域模型

1、值对象 Product(一个订单涉及的具体产品),一旦创建,就不会再变

public class Product {
    private final String productId;
    private final String name;
    private final double price;

    // 只有构造方法和get方法
}

2、实体 OrderItem 订单项

public class OrderItem {
    private final Product product;
    private int quantity; // 数量

    // 构造方法 getter方法

    public void setQuantity(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero.");
        }
        this.quantity = quantity;
    }

    public double getTotalPrice() {
        return product.getPrice() * quantity;
    }
}

3、聚合根 Order 订单

public class Order {
    private final String orderId;
    private final List<OrderItem> items;

    // 构造方法 getter方法

    public void addItem(Product product, int quantity) {
        OrderItem item = findItemByProduct(product);
        if (item == null) {
            items.add(new OrderItem(product, quantity));
        } else {
            item.setQuantity(item.getQuantity() + quantity);
        }
    }

    public void removeItem(Product product) {
        OrderItem item = findItemByProduct(product);
        if (item != null) {
            items.remove(item);
        }
    }

    public double getTotalPrice() {
        return items.stream().mapToDouble(OrderItem::getTotalPrice).sum();
    }

    private OrderItem findItemByProduct(Product product) {
        return items.stream().filter(i -> i.getProduct().equals(product)).findFirst().orElse(null);
    }
}

4、领域服务:

public class OrderDomainService {
    // 计算订单的总价
    public double calculateTotalPrice(Order order) {
        return order.getItems().stream()
                .mapToDouble(OrderItem::getTotalPrice)
                .sum();
    }

    // 判断订单是否可以应用折扣,在这里就是判断订单的总价是否大于100
    public boolean isOrderEligibleForDiscount(Order order) {
        return order.getTotalPrice() > 100;
    }

    // 应用折扣
    public void applyDiscount(Order order, double discountRate) {
        if (isOrderEligibleForDiscount(order)) {
            double totalPrice = calculateTotalPrice(order);
            double discount = totalPrice * discountRate;
            order.setTotalPrice(totalPrice - discount);
        }
    }
}

5、 仓储服务和实现,仓储服务在领域层,实现在基础服务层。

// 接口
public interface OrderRepository {
    // 根据订单ID查找订单
    Order findById(String orderId);

    // 保存订单
    void save(Order order);

    // 删除订单
    void delete(Order order);

    // 获取所有订单
    List<Order> findAll();
}


// 实现
public class MybatisOrderRepository implements OrderRepository {

}

6、应用服务:调用领域服务和基础服务,完成流程编排

@Service
public class OrderApplicationService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private OrderDomainService orderDomainService;

    // 保存订单
    public void placeOrder(String customerId, List<OrderItem> items) {
        Order order = new Order(customerId);
        for (OrderItem item : items) {
            order.addItem(item);
        }
        double totalPrice = orderDomainService.calculateTotalPrice(order);
        if (orderDomainService.isOrderEligibleForDiscount(order)) {
            orderDomainService.applyDiscount(order, 0.9); // 9折
            // 在这里把订单的折扣信息也保存到数据库中,和订单信息分开保存
        }
        orderRepository.save(order);
    }

    // 获取订单
    public Order getOrder(String orderId) {
        return orderRepository.findById(orderId);
    }

    // 改变订单状态
    public void changeOrderStatus(String orderId, OrderStatus newStatus) {
        Order order = orderRepository.findById(orderId);
        if (order != null) {
            order.setStatus(newStatus);
            orderRepository.save(order);
        } else {
            throw new IllegalArgumentException("Order not found");
        }
    }
}

这个案例描述了在DDD中代码按照领域模型应该如何组织,没有描述的代码,基本和其他设计模式下的代码差不多。

DDD中的设计原则

设计原则:

  • 将修改操作和查询操作分离到不同的服务中,以优化性能和可扩展性。

命名规范

模块命名规范

  • xxx-api:API模块,负责定义接口和模型,通常以Request、Response、DTO等结尾,也可使用诸如client等命名风格。
  • xxx-starter:接入层负责项目启动配置和业务入口,前者包括服务配置、切面等技术能力。后者包括接口协议适配、基础参数校验、请求转发、响应结果封装、异常处理、日志打点等工作。
  • xxx-application:应用层负责用例实现,通常串联内部领域服务接口或外部服务接口,实现流程编排、聚合查询等工作流操作。
  • xxx-domain:领域层,负责业务逻辑实现,通常抽象出领域对象,来表达业务概念,承载业务行为和规则。
  • xxx-infrastructure:基础设施层,通常负责外部调用,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。

包名规范

用户接口层:interfaces

  • assembler:模型转换
  • dto:数据传输模型
  • facade:提供给外部的调用接口

应用层:application

  • servce:应用服务

领域层:domain

  • 聚合:例如订单聚合,包名就叫做order
    • entity:存放实体
    • service:领域服务
    • repository:仓储服务,定义接口,实现放在基础服务层

基础层:infrastructure

  • 聚合/repository:仓储服务的实现
  • config
  • utils

Q&A

领域和领域模型是什么?

领域,就是具体的业务,从代码设计的角度讲,一个领域,是一项可以放在某个单独的微服务中实现的业务,通过边界,划分出不同的领域,每个领域实现各自的功能

领域模型,是这个领域中的业务模型,领域模型是根据业务来建模的,是可以百分百反映业务的,领域模型的基本单位是聚合,聚合是逻辑上的概念,是由一系列实体和值对象组成,聚合是增删改查的基本单位,聚合中的实体,一个实体在代码中就是一个类,它是某个业务对象。

什么样的代码应该放到领域服务?

对于领域模型的计算,应该放在领域服务。

越过聚合根单独操作实体

聚合和聚合根是业务上紧密关联的实体的集合,这个集合中的数据需要保持状态一致。

我的使用经验,要根据业务来设计聚合,然后根据聚合来设计代码,如果聚合本身就很大,在某些情况下,可以越过聚合根,单独操作聚合中的实体,但是这种操作不能很多。不要因为代码反过来影响聚合的设计,最好的聚合就是可以直接反映业务的聚合。

数据中的表关联,使用外键合适还是使用关联表合适

如果两个实体之间是一对多的父子关系,外键或许合适,如果多对多的并列关系,只能使用关联表。

应用层直接调用仓储服务,合理吗?

在DDD中,应用层直接调用仓储是符合规范的,因为应用层负责协调应用的工作流和管理事务,而领域层专注于业务逻辑的实现。

在用户接口层调用基础层,实现缓存功能,合理吗?

合理,基础层是为每一层服务的,在用户接口层也可以调用。

领域驱动设计,如果在interfaces层定义了DTO,传递到application层,也就是application层依赖interfaces层,这种设计合理吗?

不合理,应该将 DTO 定义在应用层中,应用层的DTO可以在接口层使用,以便于接口层与外部系统交互。不过在微服务的系统中,和外部交互的DTO、接口,通常单独定义在client包下,然后接口层、应用层,共同依赖client包。

为什么分离业务模型和数据模型

  • 关注点分离:业务模型专注于业务逻辑和行为,而数据模型专注于数据存储和性能优化。这种分离允许开发者在不影响业务逻辑的情况下优化数据存储。
  • 灵活性和适应性:业务需求变化时,业务模型可能需要调整,但数据模型可以保持不变,反之亦然。这种分离可以减少变更的影响范围,提高系统的适应性。
  • 技术无关性:业务模型可以独立于具体的数据库技术,使得系统更容易迁移到不同的数据库或存储技术。
  • 测试和维护:业务模型的独立性使得单元测试更容易,因为它们不依赖于数据库。维护和演化业务逻辑时,不必担心数据存储的细节。

分离业务模型和数据模型是为了更好地应对复杂系统中的不同关注点和变化需求。虽然在某些情况下这可能会增加初期的开发工作量,但从长期来看,这种分离可以提高系统的灵活性、可维护性和适应性

领域模型和数据模型

领域模型和数据模型:

  • 领域模型:业务的核心实体,关键是清晰地表达业务语义。
  • 数据模型:数据模型负责的是数据存储,其要义是扩展性、灵活性、性能。

它们的缩写都是DDD,Data Driven Design、Domain Driven Design,数据驱动设计、领域驱动设计

数据模型负责的是数据存储,其要义是扩展性、灵活性、性能。而领域模型负责业务逻辑的实现,其要义是业务语义显性化的表达,以及充分利用OO的特性增加代码的业务表征能力。

ER模型:Entity Relationship,实体关系,一种用于数据建模的方法,帮助设计者在设计数据库时,直观地表示实体及其相互关系。它通常使用图形表示,包括以下几个基本组件:

  • 实体(Entity):实体是一个可区分的对象或概念,比如用户、产品、订单等。在ER图中,实体通常表示为矩形。
  • 属性(Attribute):属性是实体所具有的特征或性质,比如用户的姓名、电子邮件,产品的价格等。在ER图中,属性通常表示为椭圆形,并连接到实体。
  • 关系(Relationship):关系描述了实体之间的关联,比如用户与订单之间的关系。在ER图中,关系通常表示为菱形,并连接相关实体。
  • 键(Key):键是用于唯一标识实体的属性,比如用户的ID,产品的SKU等。

软件设计中的防腐层是什么?

防腐层:Anti-Corruption Layer, ACL,是一种架构模式,用于在不同的子系统或服务之间进行适配和隔离,以防止外部系统的变化或复杂性影响到核心系统。这个概念主要来源于领域驱动设计,旨在保护核心域模型不受外部系统的影响。

参考

https://www.infoq.cn/article/s_LFUlU6ZQODd030RbH9

你可能感兴趣的:(设计模式,java,开发语言,设计模式)