DDD之DP设计原则

Domain Primitive (DP) 是一种领域驱动设计(DDD)中的设计原则,旨在通过将领域中的基本概念封装为不可变的值对象(Value Object),从而提高代码的可读性、可维护性和安全性。DP 的核心思想是将领域中的原始类型(如字符串、数字)封装为具有明确语义和行为的对象。

以下是 DP 设计原则的详细解析:


1. DP 的核心概念

(1) 什么是 Domain Primitive?

  • 定义
    • DP 是领域中最基本的概念,通常是一个不可变的值对象。
    • 它封装了领域中的原始类型,并赋予其明确的语义和行为。
  • 示例
    • String 类型的电话号码封装为 PhoneNumber 类。
    • int 类型的年龄封装为 Age 类。

(2) DP 的特点

  • 不可变性(Immutable)
    • DP 对象一旦创建,其值不可更改。
  • 自验证(Self-Validating)
    • DP 对象在创建时自动验证其值的合法性。
  • 明确语义(Explicit Semantics)
    • DP 对象具有明确的领域语义,避免原始类型的模糊性。
  • 行为封装(Encapsulated Behavior)
    • DP 对象可以封装与领域相关的行为(如格式化、比较)。

2. DP 的设计原则

(1) 封装原始类型

  • 问题
    • 原始类型(如 Stringint)缺乏明确的语义,容易导致错误。
  • 解决方案
    • 将原始类型封装为 DP 对象,赋予其明确的语义。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class PhoneNumber {
    private final String value;

    public PhoneNumber(String value) {
    if (value == null || !value.matches("\\d{11}")) {
    throw new IllegalArgumentException("Invalid phone number");
    }
    this.value = value;
    }

    public String getValue() {
    return value;
    }
    }

(2) 自验证

  • 问题
    • 原始类型的值可能不合法,需要在业务逻辑中手动验证。
  • 解决方案
    • DP 对象在创建时自动验证其值的合法性。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Age {
    private final int value;

    public Age(int value) {
    if (value < 0 || value > 150) {
    throw new IllegalArgumentException("Invalid age");
    }
    this.value = value;
    }

    public int getValue() {
    return value;
    }
    }

(3) 不可变性

  • 问题
    • 可变对象可能导致意外的状态变化。
  • 解决方案
    • DP 对象一旦创建,其值不可更改。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
    this.amount = amount;
    this.currency = currency;
    }

    public BigDecimal getAmount() {
    return amount;
    }

    public Currency getCurrency() {
    return currency;
    }
    }

(4) 行为封装

  • 问题
    • 与领域相关的行为分散在业务逻辑中,导致代码重复。
  • 解决方案
    • 将与领域相关的行为封装到 DP 对象中。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Email {
    private final String value;

    public Email(String value) {
    if (value == null || !value.matches("[^@]+@[^@]+\\.[^@]+")) {
    throw new IllegalArgumentException("Invalid email");
    }
    this.value = value;
    }

    public String getDomain() {
    return value.substring(value.indexOf('@') + 1);
    }
    }

3. DP 的优势

(1) 提高代码可读性

  • DP 对象具有明确的语义,使代码更易于理解。
  • 示例:
    1
    2
    3
    4
    5
    // 原始类型
    String phoneNumber = "12345678901";

    // DP 对象
    PhoneNumber phoneNumber = new PhoneNumber("12345678901");

(2) 提高代码可维护性

  • DP 对象封装了验证逻辑和行为,减少了重复代码。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    // 原始类型
    if (phoneNumber == null || !phoneNumber.matches("\\d{11}")) {
    throw new IllegalArgumentException("Invalid phone number");
    }

    // DP 对象
    PhoneNumber phoneNumber = new PhoneNumber("12345678901");

(3) 提高代码安全性

  • DP 对象在创建时自动验证其值的合法性,避免了非法值的传播。
  • 示例:
    1
    2
    3
    4
    5
    // 原始类型
    int age = -1; // 非法值

    // DP 对象
    Age age = new Age(-1); // 抛出异常

4. DP 的实现示例

以下是一个完整的 DP 实现示例:

(1) DP 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class PhoneNumber {
private final String value;

public PhoneNumber(String value) {
if (value == null || !value.matches("\\d{11}")) {
throw new IllegalArgumentException("Invalid phone number");
}
this.value = value;
}

public String getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PhoneNumber that = (PhoneNumber) o;
return value.equals(that.value);
}

@Override
public int hashCode() {
return value.hashCode();
}

@Override
public String toString() {
return "PhoneNumber{" +
"value='" + value + '\'' +
'}';
}
}

(2) 使用 DP 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User {
private final String name;
private final PhoneNumber phoneNumber;

public User(String name, PhoneNumber phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}

public String getName() {
return name;
}

public PhoneNumber getPhoneNumber() {
return phoneNumber;
}
}

(3) 测试 DP 类

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
PhoneNumber phoneNumber = new PhoneNumber("12345678901");
User user = new User("Alice", phoneNumber);
System.out.println(user.getPhoneNumber().getValue()); // 输出:12345678901
}
}

5. DP 的最佳实践

  1. 识别领域中的原始类型
    • 将领域中的原始类型(如 Stringint)封装为 DP 对象。
  2. 封装验证逻辑
    • 在 DP 对象的构造函数中验证其值的合法性。
  3. 保持不可变性
    • DP 对象一旦创建,其值不可更改。
  4. 封装领域行为
    • 将与领域相关的行为封装到 DP 对象中。
  5. 避免过度设计
    • 只在必要时使用 DP,避免过度封装。

总结

DP 设计原则通过将领域中的原始类型封装为具有明确语义和行为的值对象,提高了代码的可读性、可维护性和安全性。通过遵循 DP 的设计原则,可以构建更加健壮和清晰的领域模型。