clearwind

clearwind

首页
分类
登录 →
clearwind

clearwind

首页 分类
登录
  1. 首页
  2. 01 | 初识DDD

01 | 初识DDD

0
  • 发布于 2024-06-29
  • 15 次阅读
clearwind
clearwind

DDD 主要关注于创建与业务领域紧密相关的软件模型,以确保软件能够准确地解决实际问题。

DDD 的核心理念

  1. 领域模型(Domain Model):

  2. 领域模型是对特定业务领域知识的精确表述,它包括业务中的实体(Entities)、值对象(Value Objects)、服务(Services)、聚合(Aggregates)、聚合根(Aggregate Roots)等概念。领域模型是DDD的核心,它反映了业务专家的语言和决策。

  3. 统一语言(Ubiquitous Language):

  4. 统一语言是开发团队与业务专家共同使用的语言,它在整个项目中保持一致。统一语言确保所有人都对业务概念有着相同的理解,减少沟通成本和误解。

  5. 限界上下文(Bounded Context):

  6. 限界上下文是明确界定的系统边界,在这个边界内部有一套统一的模型和语言。不同的限界上下文之间可能有不同的模型,它们通过上下文映射(Context Mapping)来进行交互和集成。

  7. 聚合(Aggregate):

  8. 聚合是一组相关对象的集合,它们被视为数据修改的单元。每个聚合都有一个聚合根,它是外部对象与聚合内部对象交互的唯一入口。

  9. 领域服务(Domain Services):

  10. 当某些行为不自然属于任何实体或值对象时,这些行为可以被定义为领域服务。领域服务通常表示领域中的一些操作或业务逻辑。

  11. 应用服务(Application Services):

  12. 应用服务是软件的一部分,它们协调领域对象来执行任务。它们负责应用程序的工作流程,但不包含业务规则或知识。

  13. 基础设施(Infrastructure):

  14. 基础设施包括为领域模型提供持久化机制(如数据库)、消息传递、应用程序的配置等技术组件。

  15. 领域事件(Domain Events):

  16. 领域事件是领域中发生的有意义的业务事件,它们可以触发其他子系统的反应或流程。

DDD 的目标是通过将软件的关注点集中在核心领域上,并通过丰富的领域模型来管理复杂性,从而提高软件的质量和维护性。DDD 强调与业务专家的紧密合作,以确保软件解决方案能够准确反映业务需求。通过这种方法,软件开发团队可以创建出更加灵活、可扩展且与业务紧密结合的系统。

其中DDD所提到的软件设计方法涵盖了;范式、模型、框架、方法论,主要活动包括建模、测试、工程、开发、部署、维护。

软件设计方法是指一系列用于指导软件开发过程的原则、概念和实践。这些方法通常包括范式、模型、框架和方法论。下面我将分别介绍这些概念以及软件设计的主要活动。

范式 (Paradigms)

范式是指软件设计和开发的基本风格或哲学。它通常定义了编程的基本原则和模式。常见的软件设计范式包括:

  1. 结构化编程:强调程序结构的重要性,使用顺序、选择和循环控制结构。

  2. 面向对象编程 (OOP):基于对象的概念,将数据和处理数据的方法封装在一起。

  3. 函数式编程:将计算视为数学函数的评估,避免状态改变和可变数据。

  4. 事件驱动编程:以事件为中心,响应用户操作、消息或其他系统事件。

  5. 面向接口编程:功能效用基于接口

模型 (Models)

模型是对软件系统的抽象表示,用于帮助理解、设计和测试系统。常用的软件设计模型包括:

  1. UML (统一建模语言):一套图形化的建模语言,用于描述、设计和文档化软件项目。

  2. ER模型 (实体-关系模型):用于数据库设计,描述数据的实体及其之间的关系。

  3. 状态机模型:描述系统可能的状态、事件和在这些事件发生时的转换。

Domain(领域)的概念

在DDD中,领域是指具体业务领域的知识、业务逻辑、数据以及业务规则的集合。它是软件要解决问题的业务环境,通常由一系列子领域组成,每个子领域代表业务中的一个特定部分。

领域的特性

  1. 业务中心:领域是围绕业务需求和业务规则构建的,它是软件设计的核心。

  2. 模型驱动:领域模型是对业务知识的抽象,它通过领域实体、值对象、服务、聚合等概念来表达。

  3. 语言一致性:领域模型的构建基于统一语言(Ubiquitous Language),这是开发团队与业务专家共同使用的语言,确保沟通无歧义。

  4. 边界清晰:领域模型定义了清晰的边界,这些边界划分了不同的子领域和聚合,有助于管理复杂性和维护性。

领域的用途

  1. 业务逻辑的封装:领域模型封装了业务逻辑,使得业务规则和数据操作集中管理,便于理解和维护。

  2. 沟通工具:领域模型作为开发团队与业务专家之间的共同语言,有助于提高沟通效率,确保软件开发紧密跟随业务需求。

  3. 软件设计的基础:领域模型是软件设计的基础,它指导着软件的架构和实现。

实现手段

  1. 实体(Entity):具有唯一标识的领域对象,代表业务中的实体。

  2. 值对象(Value Object):描述领域中的一些特性或概念,没有唯一标识,通常是不可变的。

  3. 聚合(Aggregate):一组相关的实体和值对象的集合,它们一起构成一个数据和业务规则的单元。

  4. 领域服务(Domain Service):在领域模型中执行特定业务逻辑的无状态服务,通常操作多个实体或聚合。

  5. 领域事件(Domain Event):表示领域中发生的重要业务事件,用于解耦系统的不同部分。

  6. 仓储(Repository):提供对聚合根的持久化操作,如保存和检索,通常与数据库交互。

  7. 领域适配器(Domain Adapter):领域适配器是适配器模式在DDD中的应用,它的目的是使得领域模型能够与外部系统或技术细节进行交互,而不会受到污染。

  8. 工厂(Factory):用于创建复杂的聚合或实体,封装创建逻辑。如 OpenAi项目、Lottery 项目都运用了工厂,也包括如 chatglm-sdk-java 的开发,就是会话模型结构用工厂对外提供服务。

聚合对象

聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。

关键:聚合内实现事务一致性、聚合外实现最终一致性。

特性

  1. 一致性边界:聚合确保其内部对象的状态变化是一致的。当对聚合内的对象进行操作时,这些操作必须保持聚合内所有对象的一致性。

  2. 根实体:每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。

  3. 事务边界:聚合也定义了事务的边界。在聚合内部,所有的变更操作应该是原子的,即它们要么全部成功,要么全部失败,以此来保证数据的一致性。

用途

  1. 封装业务逻辑:聚合通过将相关的对象和操作封装在一起,提供了一个清晰的业务逻辑模型,有助于业务规则的实施和维护。

  2. 保证一致性:聚合确保内部状态的一致性,通过定义清晰的边界和规则,聚合可以在内部强制执行业务规则,从而保证数据的一致性。

  3. 简化复杂性:聚合通过组织相关的对象,简化了领域模型的复杂性。这有助于开发者更好地理解和扩展系统。

实现手段

  1. 定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。

  2. 限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。

  3. 设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。

  4. 封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。

  5. 持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。

以下是一个简化的Java代码示例,展示了如何在DDD中实现一个聚合。在这个例子中,我们将创建一个简单的订单系统,其中包含订单聚合(Order Aggregate)和订单项(OrderItem)作为内部实体。订单聚合根(Order)将封装所有业务规则,并通过聚合根进行所有的交互。

首先,定义实体和值对象的基类:

public abstract class BaseEntity {

    protected Long id;

    public Long getId() {
        return id;
    }
}

然后,定义订单项(OrderItem)作为实体:

public class OrderItem extends BaseEntity {

    private String productName;
    private int quantity;
    private double price;

    public OrderItem(String productName, int quantity, double price) {
        this.productName = productName;
        this.quantity = quantity;
        this.price = price;
    }

    public double getTotalPrice() {
        return quantity * price;
    }
    //省略getter和setter方法
}

接下来,定义订单聚合根(Order):


public class OrderAggregate extends BaseEntity {

    private List<OrderItem> orderItems;
    private String customerName;
    private boolean isPaid;

    public OrderAggregate(String customerName) {
        this.customerName = customerName;
        this.orderItems = new ArrayList<>();
        this.isPaid = false;
    }

    public void addItem(OrderItem item) {

        //业务规则:订单未支付时才能添加订单项
        if (!isPaid) {
            orderItems.add(item);
        } else {
            throw new IllegalStateException("Can not add items to apaid order.");
        }

    }

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

    public void markAsPaid() {
        //业务规则:订单总金额必须大于0才能标记为已支付
        if (getTotalAmount() > 0) {
            isPaid = true;
        } else {
            throw new IllegalStateException("Order total must begreater than 0 to bepaid.");
        }
    }

    //省略getter和setter方法
}

最后,创建一个订单,并添加一些订单项:

public class OrderDemo{

    public static void main(String[] args){

        //创建订单聚合
        OrderAggregate orderAggregate= new OrderAggregate("Clearwind");
        //添加订单项
        orderAggregate.addItem(new OrderItem("手机",1,1000.00));
        orderAggregate.addItem(new OrderItem("数据线",2,25.00));
        //获取订单总金额
        System.out.println("Total amount:" + orderAggregate.getTotalAmount());
        //标记订单为已支付
        orderAggregate.markAsPaid();
    }

}

实体

实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。

特性

  1. 唯一标识:实体具有一个可以区分其他实体的标识符。这个标识符可以是一个ID、一个复合键或者是一个自然键,关键是它能够唯一地标识实体实例。

  2. 领域标识:实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。

  3. 委派标识:在某些情况下,实体的标识可能是由ORM(对象关系映射)框架自动生成的,如数据库中的自增主键。这种标识符虽然可以唯一标识实体,但它并不直接来源于业务领域。

用途

  1. 表达业务概念:实体用于在软件中表达具体的业务概念,如用户、订单、交易等。通过实体的属性和行为,可以描述这些业务对象的特征和能力。

  2. 封装业务逻辑:实体不仅仅承载数据,还封装了业务规则和逻辑。这些逻辑包括验证数据的有效性、执行业务规则、计算属性值等。这样做的目的是保证业务逻辑的集中和一致性。

  3. 保持数据一致性:实体负责维护自身的状态和数据一致性。它确保自己的属性和关联关系在任何时候都是正确和完整的,从而避免数据的不一致性。

实现手段

在实现实体时,通常会采用以下手段:

  1. 定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。

  2. 实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。

  3. 封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。

  4. 使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。

  5. 实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。

  6. 使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。

通过上述手段,实体在DDD架构中扮演着重要的角色,它不仅代表了业务概念,还封装了业务逻辑,并通过其唯一标识确保了数据的一致性。

以下是一个简单的Java代码示例,展示了如何在领域驱动设计(DDD)中实现一个实体。我们将创建一个User实体,它代表了一个用户,具有唯一的用户ID、姓名和电子邮件地址,并且可以执行一些基本的行为。

public class UserEntity {
    //实体的唯一标识符
    private final UUID id;
    //用户的状态属性
    private String name;
    private String email;
    //构造函数,用于创建实体实例

    public UserEntity(UUID id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
        //可以在这里添加验证逻辑,确保创建的实体是有效的
    }

    //实体的行为方法,例如更新用户的姓名
    public void updateName(String newName) {
        //可以在这里添加业务规则,例如验证姓名的格式
        this.name = newName;
    }
    //实体的行为方法,例如更新用户的电子邮件地址
    public void updateEmail(String newEmail) {
        //可以在这里添加业务规则,例如验证电子邮件地址的格式
        this.email = newEmail;
    }

    //Getter方法
    public UUID getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    //实体的equals和hashCode方法,基于唯一标识符实现

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserEntity user = (UserEntity) o;
        return id.equals(user.id);
    }

    //toString方法,用于打印实体信息
    @Override
    public String toString() {
        return "UserEntity{" +
                "id=" + id +
                ",name='" + name + '\'' +
                ",email='" + email + '\'' +
                '}';
    }

}

public class UserEntityDemo {
    public static void main(String[] args) {

        //创建一个新的用户实体
        UserEntity user = new UserEntity(UUID.randomUUID(), "clearwind", "clearwind@qq.com");
        //打印用户信息
        System.out.println(user);
        //更新用户的姓名
        user.updateName("clearwind111");
        //打印更新后的用户信息
        System.out.println(user);
        //更新用户的电子邮件地址
        user.updateEmail("clearwind111@qq.com");
        //打印更新后的用户信息
        System.out.println(user);
    }

}

在这个例子中,User类代表了用户实体,它有一个唯一的标识符id,这个标识符在实体的整个生命周期中保持不变。name和email是用户的状态属性,updateName和updateEmail是封装了业务逻辑的行为方法。

值对象

在领域驱动设计(Domain-Driven Design, DDD)中,值对象(Value Object)是一个核心概念,用于封装和表示领域中的概念,其特点是它们描述了领域中的某些属性或度量,但不具有唯一标识。

值对象 = 值 + 对象,用于描述对象属性的值,表示具体固定不变的属性值信息。

概念

值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。

特性

  1. 不可变性(Immutability):值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。

  2. 等价性(Equality):值对象的等价性不是基于身份或引用,而是基于对象的属性值。如果两个值对象的所有属性值都相等,那么这两个对象就被认为是等价的。

  3. 替换性(Replaceability):由于值对象是不可变的,任何需要改变值对象的操作都会导致创建一个新的值对象实例,而不是修改现有的实例。

  4. 侧重于描述事物的状态:值对象通常用来描述事物的状态,而不是事物的唯一身份。

  5. 可复用性(Reusability):值对象可以在不同的领域实体或其他值对象中重复使用。

用途

值对象的用途非常广泛,它们可以用来表示:

  1. 金额和货币(如价格、工资、费用等)

  2. 度量和数据(如重量、长度、体积等)

  3. 范围或区间(如日期范围、温度区间等)

  4. 复杂的数学模型(如坐标、向量等)

  5. 任何其他需要封装的属性集合

实现手段

在实现值对象时,通常会遵循以下几个步骤:

  1. 定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。

  2. 重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。

  3. 提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。

  4. 使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。

  5. 考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。

示例

以订单状态为例,可以定义一个值对象来表示不同的状态:

public enum OrderStatusVO {

    PLACED(0, "下单"),
    PAID(1, "支付"),
    COMPLETED(2, "完成"),
    CANCELLED(3, "退单");

    private final int code;

    private final String description;

    OrderStatusVO(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    //根据code获取对应的OrderStatus
    public static OrderStatusVO fromCode(int code) {
        for (OrderStatusVO status : OrderStatusVO.values()) {
            if (status.getCode() == code) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid code for OrderStatus:" + code);
    }
}

在这个例子中,OrderStatusVO是一个枚举类型的值对象,它封装了订单状态的代码和描述。它是不可变的,并且提供了基于属性值的等价性。通过定义一个枚举,我们可以确保订单状态的值是受限的,并且每个状态都有一个明确的含义。

在数据库中,订单状态可能会以整数形式存储(例如,0表示下单,1表示支付等)。在应用程序中,我们可以使用OrderStatusVO枚举来确保我们在代码中使用的是类型安全的值,而不是裸露的整数。这样可以减少错误,并提高代码的可读性和可维护性。

当需要将订单状态存储到数据库中时,我们可以存储枚举的code值。当从数据库中读取订单状态时,我们可以使用fromCode方法来将整数值转换回OrderStatusVO枚举,这样就可以在代码中使用丰富的枚举类型而不是简单的整数。

值对象也可以用来表示更复杂的结构,比如一个地址:


public final class Address {
    private final String street;
    private final String city;
    private final String zipCode;
    private final String country;

    public Address(String street, String city, String zipCode, String country) {
        //这里可以添加验证逻辑以确保地址的有效性
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
        this.country = country;
    }

    //只读访问器
    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public String getZipCode() {
        return zipCode;
    }

    public String getCountry() {
        return country;
    }

    //重写equals和hashCode方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return street.equals(address.street) &&
                city.equals(address.city) &&
                zipCode.equals(address.zipCode) &&
                country.equals(address.country);
    }

    @Override
    public int hashCode() {
        return Objects.hash(street, city, zipCode, country);
    }

}

在这个例子中,AddressVO是一个不可变的值对象,它封装了一个地址的所有部分。它提供了只读访问器,并且重写了equals和hashCode方法以确保基于属性值的等价性。这样的设计有助于确保地址的一致性,并且可以在不同的实体之间重复使用,例如用户和商店都可能有地址。

总的来说,值对象是DDD中用于封装领域概念的重要工具,它们通过提供不可变性、基于属性的等价性和替换性来帮助构建一个清晰、一致和可维护的领域模型。

标签: #DDD 1
目录
  • clearwind
  • 微信小程序

导航菜单

  • 首页
  • 分类
Copyright © 2024 your company All Rights Reserved. Powered by clearwind.
皖ICP备19023482号