C++ 领域驱动设计:在复杂业务逻辑中利用 C++ 类型系统表达业务约束

欢迎来到本次关于“C++ 领域驱动设计:在复杂业务逻辑中利用 C++ 类型系统表达业务约束”的讲座。在当今软件开发领域,面对日益增长的业务复杂性,领域驱动设计(DDD)提供了一套强大的思想框架来帮助我们理解、建模和实现复杂的业务系统。而C++,作为一门以其高性能和底层控制能力著称的语言,似乎在传统上与DDD的抽象和建模关注点有所距离。然而,这是一种误解。事实上,C++强大的静态类型系统,如果被恰当地利用,能够成为在代码层面强制执行业务规则和约束的强大工具,从而构建出更健壮、更易于维护和更符合领域模型的系统。

本次讲座将深入探讨如何将DDD的核心思想与C++的类型系统特性相结合。我们将看到,通过精心设计的类、结构体、枚举、模板以及现代C++的各种语言特性,我们不仅可以实现业务逻辑,更能将业务约束“编码”进类型定义本身,使得那些在业务层面被认为是“非法”或“不可能”的状态,在编译期就被C++类型系统所拒绝,从而显著提高软件质量和开发效率。


1. 领域驱动设计 (DDD) 核心概念的C++视角

在深入探讨类型系统之前,我们首先快速回顾DDD的一些核心概念,并思考它们在C++语境下如何落地。

1.1 泛在语言 (Ubiquitous Language)

泛在语言是DDD的基石,它要求团队成员(领域专家、开发人员、测试人员等)使用一套共同的、精确的语言来描述业务领域。在C++中,这意味着我们的类名、函数名、变量名、枚举值等都应该直接反映泛在语言中的术语。避免使用技术术语来命名业务概念,例如,不要用int customerId,而应该用CustomerId customer_id

1.2 实体 (Entity) 与 值对象 (Value Object)

这是DDD中最基本的构建块。

  • 实体 (Entity): 具有唯一标识和生命周期,即使属性发生变化,它仍然是同一个实体。例如,一个Customer或一个Order。在C++中,实体通常通过一个唯一的ID(强类型ID)来标识,其状态可以被修改。

    // 实体:Order
    class Order
    {
    public:
        // 强类型ID
        struct OrderId {
            std::string value;
            explicit OrderId(std::string id) : value(std::move(id)) {}
            bool operator==(const OrderId& other) const { return value == other.value; }
            bool operator!=(const OrderId& other) const { return !(*this == other); }
            // 提供哈希函数以便在std::map等容器中使用
            struct Hasher {
                std::size_t operator()(const OrderId& id) const {
                    return std::hash<std::string>{}(id.value);
                }
            };
        };
    
        // 订单状态(值对象)
        enum class OrderStatus {
            Pending,
            Processing,
            Shipped,
            Delivered,
            Cancelled
        };
    
        Order(OrderId id, Customer::CustomerId customerId, OrderStatus status = OrderStatus::Pending);
    
        // 行为
        void addItem(ProductId productId, Quantity quantity);
        void updateStatus(OrderStatus newStatus);
        // ... 其他业务行为
    
        // 访问器
        const OrderId& getId() const { return id_; }
        Customer::CustomerId getCustomerId() const { return customerId_; }
        OrderStatus getStatus() const { return status_; }
        // ...
    
    private:
        OrderId id_;
        Customer::CustomerId customerId_; // 另一个强类型ID
        OrderStatus status_;
        std::vector<LineItem> items_; // LineItem可能是值对象或实体
        // ... 其他私有成员
    };
  • 值对象 (Value Object): 描述事物的特征,没有唯一标识,完全由其属性值定义。当属性值相同时,两个值对象被认为是相同的。它们应该是不可变的。例如,MoneyAddressQuantity。在C++中,值对象通常通过structclass实现,其所有成员都是const或通过构造函数一次性初始化且不可更改。它们应该重载比较运算符(==!=),并且易于复制。

    // 值对象:Money
    class Money {
    public:
        enum class Currency { USD, EUR, GBP, JPY, CNY };
    
        Money(double amount, Currency currency)
            : amount_(amount), currency_(currency) {
            // 业务约束:金额不能为负
            if (amount_ < 0) {
                throw std::invalid_argument("Amount cannot be negative.");
            }
        }
    
        // 不可变性:没有setter
        double getAmount() const { return amount_; }
        Currency getCurrency() const { return currency_; }
    
        // 值对象的相等性基于其属性
        bool operator==(const Money& other) const {
            return amount_ == other.amount_ && currency_ == other.currency_;
        }
        bool operator!=(const Money& other) const {
            return !(*this == other);
        }
    
        // 业务行为:加法,返回新的Money值对象
        Money add(const Money& other) const {
            if (currency_ != other.currency_) {
                throw std::invalid_argument("Cannot add different currencies.");
            }
            return Money(amount_ + other.amount_, currency_);
        }
        // ... 其他运算
    
    private:
        double amount_; // 注意浮点数精度问题,实际应用中可能用Decimal类型
        Currency currency_;
    };

1.3 聚合 (Aggregate)

聚合是DDD中用于封装实体和值对象,并强制不变量的关键概念。它定义了一个边界,在这个边界内,所有对象都必须通过聚合根(Aggregate Root)进行访问。聚合根保证了聚合内部所有对象的业务规则一致性。

在C++中实现聚合:

  • 聚合根作为入口: 聚合根是外部对象唯一可以引用的对象。聚合内部的其他实体和值对象不应该直接暴露给外部。
  • 私有成员和智能指针: 聚合根通常通过私有成员来拥有其内部对象,可能使用std::unique_ptr来表达所有权关系,或者std::vector来管理集合。
  • 行为封装: 聚合内的业务操作都应该通过聚合根的方法来执行,聚合根负责维护聚合的不变量。
// 假设 Product 和 LineItem 是聚合内部的实体或值对象
// Product可能是一个独立的聚合根,但LineItem通常是Order聚合的一部分

// LineItem (值对象)
class LineItem {
public:
    struct ProductId { // 强类型ProductId
        std::string value;
        explicit ProductId(std::string id) : value(std::move(id)) {}
        // ... 比较运算符和Hasher
    };

    LineItem(ProductId productId, Quantity quantity, Money unitPrice)
        : productId_(std::move(productId)), quantity_(quantity), unitPrice_(unitPrice) {
        // 业务约束:数量必须大于0
        if (quantity_.getValue() <= 0) {
            throw std::invalid_argument("Quantity must be positive.");
        }
    }

    ProductId getProductId() const { return productId_; }
    Quantity getQuantity() const { return quantity_; }
    Money getUnitPrice() const { return unitPrice_; }
    Money getTotalPrice() const { return unitPrice_.multiply(quantity_.getValue()); } // 假设Money有multiply方法

private:
    ProductId productId_;
    Quantity quantity_; // 强类型Quantity
    Money unitPrice_;
};

// Order (聚合根)
class Order
{
public:
    // OrderId, OrderStatus, CustomerId 定义同前
    // ...

    Order(OrderId id, Customer::CustomerId customerId)
        : id_(std::move(id)), customerId_(std::move(customerId)), status_(OrderStatus::Pending) {}

    // 聚合根提供业务行为,确保不变量
    void addItem(LineItem::ProductId productId, Quantity quantity, Money unitPrice) {
        // 业务约束:已发货或已取消的订单不能添加商品
        if (status_ == OrderStatus::Shipped || status_ == OrderStatus::Cancelled) {
            throw std::runtime_error("Cannot add items to a shipped or cancelled order.");
        }
        items_.emplace_back(productId, quantity, unitPrice);
        // ... 更新总价等
    }

    void updateStatus(OrderStatus newStatus) {
        // 业务约束:状态流转规则
        // 例如:Pending -> Processing -> Shipped -> Delivered
        //       Processing -> Cancelled
        if (status_ == OrderStatus::Pending && newStatus == OrderStatus::Processing) {
            status_ = newStatus;
        } else if (status_ == OrderStatus::Processing &&
                   (newStatus == OrderStatus::Shipped || newStatus == OrderStatus::Cancelled)) {
            status_ = newStatus;
        } else if (status_ == OrderStatus::Shipped && newStatus == OrderStatus::Delivered) {
            status_ = newStatus;
        } else {
            throw std::runtime_error("Invalid order status transition.");
        }
    }

    const OrderId& getId() const { return id_; }
    Customer::CustomerId getCustomerId() const { return customerId_; }
    OrderStatus getStatus() const { return status_; }
    const std::vector<LineItem>& getItems() const { return items_; } // 暴露不可修改的内部列表

private:
    OrderId id_;
    Customer::CustomerId customerId_;
    OrderStatus status_;
    std::vector<LineItem> items_; // 聚合内部的LineItem值对象
    // ...
};

1.4 领域服务 (Domain Service)

当某个操作不属于任何一个实体或值对象,但又属于领域逻辑时,它就应该被实现为领域服务。领域服务是无状态的,它协调多个聚合或执行复杂的计算。

在C++中,领域服务可以是:

  • 自由函数: 如果操作简单且不依赖于任何状态。
  • 静态成员函数: 如果操作与某个类在逻辑上相关但不需实例化。
  • 独立的类: 通常通过依赖注入来接收其所需的聚合仓储或其他服务。
// 领域服务:OrderPlacementService
class OrderPlacementService {
public:
    // 假设我们有 OrderRepository 和 ProductRepository 接口
    // OrderPlacementService(IOrderRepository& orderRepo, IProductRepository& productRepo);

    Order placeOrder(Customer::CustomerId customerId, const std::vector<LineItemData>& itemsData) {
        // 1. 生成新的订单ID
        Order::OrderId newOrderId = generateNewOrderId(); // 假设有ID生成器

        // 2. 创建订单聚合根
        Order newOrder(newOrderId, customerId);

        // 3. 验证商品并添加到订单
        for (const auto& itemData : itemsData) {
            // 从 ProductRepository 获取商品信息,验证是否存在,获取价格
            // Product product = productRepo_.findById(itemData.productId);
            // newOrder.addItem(itemData.productId, itemData.quantity, product.getPrice());
            // 简化示例,直接添加
            newOrder.addItem(itemData.productId, itemData.quantity, Money(10.0, Money::Currency::USD));
        }

        // 4. 持久化订单 (通过 OrderRepository)
        // orderRepo_.save(newOrder);

        // 5. 发布领域事件 (OrderPlacedEvent)

        return newOrder;
    }

private:
    Order::OrderId generateNewOrderId() {
        // 实际中可能通过UUID生成器或其他策略
        return Order::OrderId("ORD-" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()));
    }
    // IOrderRepository& orderRepo_;
    // IProductRepository& productRepo_;
};

2. 利用C++类型系统表达业务约束的策略

现在,我们进入本次讲座的核心:如何巧妙地利用C++的类型系统来编码业务约束,使代码本身成为业务规则的守护者。

2.1 强类型ID (Strongly Typed IDs)

问题: 在传统的C++代码中,我们经常看到std::string userId;int productId;这样的定义。这种“原始类型痴迷”导致了几个问题:

  1. 类型不安全: userIdproductId都是std::string,编译器无法区分它们,可能会意外地将一个产品的ID赋给用户ID。
  2. 缺乏上下文: 仅仅看到std::string,我们不知道它代表什么,是名称、描述还是ID?
  3. 无法附加行为: 无法为ID类型定义特定的验证或格式化行为。

解决方案: 为每种业务ID创建独立的强类型结构体或类。

示例:

// Bad: 原始类型ID
// void processOrder(std::string orderId, std::string customerId);

// Good: 强类型ID
// 定义通用的强类型ID基类(可选,但推荐)
template<typename Tag>
struct StrongId {
    std::string value;

    explicit StrongId(std::string val) : value(std::move(val)) {
        // 可以在这里添加通用的ID格式验证
        if (value.empty()) {
            throw std::invalid_argument("ID cannot be empty.");
        }
    }

    // 允许隐式转换为const std::string&,方便打印或传递给底层库
    operator const std::string&() const { return value; }

    bool operator==(const StrongId& other) const { return value == other.value; }
    bool operator!=(const StrongId& other) const { return !(*this == other); }
    bool operator<(const StrongId& other) const { return value < other.value; } // 允许在std::map中使用

    struct Hasher {
        std::size_t operator()(const StrongId& id) const {
            return std::hash<std::string>{}(id.value);
        }
    };
};

// 特定业务ID的定义
struct OrderIdTag {};
using OrderId = StrongId<OrderIdTag>;

struct CustomerIdTag {};
using CustomerId = StrongId<CustomerIdTag>;

struct ProductIdTag {};
using ProductId = StrongId<ProductIdTag>;

// 使用强类型ID的函数签名
void processOrder(const OrderId& orderId, const CustomerId& customerId) {
    std::cout << "Processing order " << orderId.value << " for customer " << customerId.value << std::endl;
    // ...
}

void test_strong_ids() {
    OrderId orderId1{"ORD-123"};
    CustomerId customerId1{"CUST-456"};
    ProductId productId1{"PROD-789"};

    processOrder(orderId1, customerId1); // OK

    // processOrder(customerId1, orderId1); // 编译错误!类型不匹配,业务约束在编译期强制执行
    // processOrder(orderId1, productId1); // 编译错误!

    OrderId orderId2{"ORD-123"};
    if (orderId1 == orderId2) { // 值相等性
        std::cout << "Order IDs are equal." << std::endl;
    }
}

通过StrongId模板和Tag类型,我们创建了编译期可区分的ID类型。这不仅提高了类型安全性,还通过构造函数强制了ID非空的业务约束。

2.2 封装原始类型 (Primitive Obsession) 为值对象

问题: 许多业务概念,如金额、数量、百分比、邮箱地址等,常常被简单地用doubleintstd::string等原始类型表示。这导致:

  1. 丢失业务语义: double price;无法区分是商品单价、总价还是成本。
  2. 验证逻辑分散: 验证金额是否为正、邮箱格式是否正确等逻辑会散布在代码库的各个角落,容易遗漏和重复。
  3. 操作不安全: 直接对原始类型进行算术运算可能不符合业务规则(例如,不同货币不能直接相加)。

解决方案: 将这些原始类型封装成具有明确业务含义和行为的值对象。

示例:

// Bad: 原始类型
// double price = 10.5;
// int quantity = 5;
// std::string email = "bad-email";

// Good: 值对象

// Quantity 值对象
class Quantity {
public:
    explicit Quantity(int value) : value_(value) {
        if (value_ <= 0) { // 业务约束:数量必须大于0
            throw std::invalid_argument("Quantity must be positive.");
        }
    }

    int getValue() const { return value_; }

    Quantity add(const Quantity& other) const { return Quantity(value_ + other.value_); }
    Quantity subtract(const Quantity& other) const {
        int result = value_ - other.value_;
        if (result <= 0) { // 业务约束:数量不能为负或零
            throw std::runtime_error("Resulting quantity must be positive.");
        }
        return Quantity(result);
    }

    bool operator==(const Quantity& other) const { return value_ == other.value_; }
    bool operator!=(const Quantity& other) const { return !(*this == other); }
    // ... 比较运算符

private:
    int value_;
};

// EmailAddress 值对象
class EmailAddress {
public:
    explicit EmailAddress(std::string email) : value_(std::move(email)) {
        // 业务约束:邮箱格式验证
        if (!isValidEmail(value_)) {
            throw std::invalid_argument("Invalid email address format: " + value_);
        }
    }

    const std::string& getValue() const { return value_; }

    bool operator==(const EmailAddress& other) const { return value_ == other.value_; }
    bool operator!=(const EmailAddress& other) const { return !(*this == other); }

private:
    std::string value_;

    bool isValidEmail(const std::string& email) const {
        // 简化验证,实际应使用更健壮的正则表达式
        return email.find('@') != std::string::npos && email.find('.') != std::string::npos;
    }
};

// 利用 C++11 用户定义字面量 (User-Defined Literals) 增强可读性
// 定义 Money 值对象
class Money {
public:
    enum class Currency { USD, EUR, GBP, CNY };
    Money(long long cents, Currency currency) : cents_(cents), currency_(currency) {
        if (cents_ < 0) throw std::invalid_argument("Amount cannot be negative.");
    }

    long long getCents() const { return cents_; }
    Currency getCurrency() const { return currency_; }

    Money operator+(const Money& other) const {
        if (currency_ != other.currency_) throw std::invalid_argument("Cannot add different currencies.");
        return Money(cents_ + other.cents_, currency_);
    }
    Money operator*(int multiplier) const { return Money(cents_ * multiplier, currency_); }

    bool operator==(const Money& other) const { return cents_ == other.cents_ && currency_ == other.currency_; }

private:
    long long cents_; // 使用长整型表示分,避免浮点数精度问题
    Currency currency_;
};

// 用户定义字面量,方便创建Money对象
namespace money_literals {
    Money operator"" _usd(long double amount) { return Money(static_cast<long long>(amount * 100), Money::Currency::USD); }
    Money operator"" _eur(long double amount) { return Money(static_cast<long long>(amount * 100), Money::Currency::EUR); }
}
using namespace money_literals; // 引入字面量

void test_value_objects() {
    try {
        Quantity q1(5);
        Quantity q2(3);
        Quantity q_sum = q1.add(q2);
        std::cout << "Quantity sum: " << q_sum.getValue() << std::endl; // Output: 8

        // Quantity q_invalid(0); // 抛出 std::invalid_argument 异常

        EmailAddress email1("[email protected]");
        // EmailAddress email_invalid("bad-email"); // 抛出 std::invalid_argument 异常

        Money price1 = 10.50_usd; // 使用用户定义字面量
        Money price2 = 2.75_usd;
        Money total_price = price1 + price2;
        std::cout << "Total price: " << total_price.getCents() / 100.0 << " USD" << std::endl; // Output: 13.25 USD

        // Money mixed_currency = 10.0_usd + 5.0_eur; // 抛出 std::invalid_argument 异常
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

通过值对象,业务约束(如数量必须为正、邮箱格式正确、不同货币不能相加)被封装在类型内部,并在对象创建时强制执行。这使得业务逻辑更清晰、更安全,并减少了重复验证代码。

2.3 状态机与枚举 (State Machines and Enums)

问题: 许多业务实体都有明确的生命周期和状态转换规则。如果使用裸整数或字符串来表示状态,容易出现:

  1. 非法状态: 允许将实体设置为业务上不可能的状态。
  2. 非法转换: 允许实体从一个状态直接跳到另一个不被允许的状态。
  3. 可读性差: if (status == 1)不如if (status == OrderStatus::Pending)清晰。

解决方案: 使用enum class来表示状态,并在实体的方法中封装状态转换逻辑。利用现代C++的std::variantstd::visit可以更进一步地实现编译期强制的状态行为。

示例:

// Bad: 裸整数状态
// enum OrderState { PENDING = 0, PROCESSING = 1, SHIPPED = 2 };
// int currentOrderState = PENDING;
// if (currentOrderState == 0 && new_state == 2) { /* 允许非法跳过 */ }

// Good: 强类型枚举和封装状态转换
enum class OrderStatus {
    Pending,
    Processing,
    Shipped,
    Delivered,
    Cancelled
};

class Order {
public:
    // ... 构造函数和getId等

    void updateStatus(OrderStatus newStatus) {
        // 业务约束:状态转换规则
        switch (status_) {
            case OrderStatus::Pending:
                if (newStatus == OrderStatus::Processing) {
                    status_ = newStatus;
                } else {
                    throw std::runtime_error("Invalid status transition from Pending.");
                }
                break;
            case OrderStatus::Processing:
                if (newStatus == OrderStatus::Shipped || newStatus == OrderStatus::Cancelled) {
                    status_ = newStatus;
                } else {
                    throw std::runtime_error("Invalid status transition from Processing.");
                }
                break;
            case OrderStatus::Shipped:
                if (newStatus == OrderStatus::Delivered) {
                    status_ = newStatus;
                } else {
                    throw std::runtime_error("Invalid status transition from Shipped.");
                }
                break;
            case OrderStatus::Delivered:
            case OrderStatus::Cancelled:
                // 已完成或已取消的订单不能再更改状态
                throw std::runtime_error("Cannot change status of a " +
                                         (status_ == OrderStatus::Delivered ? "delivered" : "cancelled") + " order.");
        }
        std::cout << "Order " << id_.value << " status updated to " << static_cast<int>(status_) << std::endl;
    }

    OrderStatus getStatus() const { return status_; }

private:
    OrderId id_;
    Customer::CustomerId customerId_;
    OrderStatus status_;
    // ...
};

void test_order_status() {
    Order::OrderId orderId{"ORD-001"};
    Customer::CustomerId customerId{"CUST-001"};
    Order order(orderId, customerId);

    std::cout << "Initial status: " << static_cast<int>(order.getStatus()) << std::endl; // 0 (Pending)

    try {
        order.updateStatus(OrderStatus::Processing); // OK
        std::cout << "Current status: " << static_cast<int>(order.getStatus()) << std::endl; // 1 (Processing)

        // order.updateStatus(OrderStatus::Delivered); // 抛出异常:Invalid status transition from Processing.

        order.updateStatus(OrderStatus::Shipped); // OK
        std::cout << "Current status: " << static_cast<int>(order.getStatus()) << std::endl; // 2 (Shipped)

        order.updateStatus(OrderStatus::Delivered); // OK
        std::cout << "Current status: " << static_cast<int>(order.getStatus()) << std::endl; // 3 (Delivered)

        // order.updateStatus(OrderStatus::Cancelled); // 抛出异常:Cannot change status of a delivered order.
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

更高级的状态机:使用 std::variantstd::visit (C++17)

对于更复杂的状态机,其中每个状态可能有不同的数据或行为,std::variant 可以用来表示实体当前处于的“具体状态”。

#include <variant>
#include <string>
#include <iostream>
#include <stdexcept>

// 定义不同状态下的特定数据
struct PendingState {};
struct ProcessingState { std::string processorId; };
struct ShippedState { std::string trackingNumber; };
struct DeliveredState {};
struct CancelledState { std::string reason; };

// 使用 std::variant 封装所有可能的状态
using OrderStateVariant = std::variant<PendingState, ProcessingState, ShippedState, DeliveredState, CancelledState>;

class OrderV2 {
public:
    OrderV2(OrderId id, CustomerId customerId) : id_(std::move(id)), customerId_(std::move(customerId)), state_(PendingState{}) {}

    void transitionToProcessing(std::string processorId) {
        if (std::holds_alternative<PendingState>(state_)) {
            state_ = ProcessingState{std::move(processorId)};
            std::cout << "Order " << id_.value << " transitioned to Processing by " << std::get<ProcessingState>(state_).processorId << std::endl;
        } else {
            throw std::runtime_error("Invalid transition to Processing.");
        }
    }

    void transitionToShipped(std::string trackingNumber) {
        if (std::holds_alternative<ProcessingState>(state_)) {
            state_ = ShippedState{std::move(trackingNumber)};
            std::cout << "Order " << id_.value << " transitioned to Shipped with tracking " << std::get<ShippedState>(state_).trackingNumber << std::endl;
        } else {
            throw std::runtime_error("Invalid transition to Shipped.");
        }
    }

    // ... 其他状态转换方法

    // 使用 visitor 模式来处理不同状态下的行为
    template<typename Visitor>
    auto visitState(Visitor&& visitor) {
        return std::visit(std::forward<Visitor>(visitor), state_);
    }

private:
    OrderId id_;
    CustomerId customerId_;
    OrderStateVariant state_; // 核心是这个变体成员
};

void test_order_variant_status() {
    OrderId orderId{"ORD-002"};
    CustomerId customerId{"CUST-002"};
    OrderV2 order(orderId, customerId);

    order.visitState([](auto& s){
        if (std::is_same_v<decltype(s), PendingState&>) std::cout << "Initial state: Pending" << std::endl;
        else std::cout << "Initial state: Unknown" << std::endl;
    });

    try {
        order.transitionToProcessing("P001");
        order.visitState([](auto& s){
            if (std::is_same_v<decltype(s), ProcessingState&>) std::cout << "Current state: Processing by " << s.processorId << std::endl;
            else std::cout << "Current state: Not Processing" << std::endl;
        });

        order.transitionToShipped("TRK-XYZ");
        order.visitState([](auto& s){
            if (std::is_same_v<decltype(s), ShippedState&>) std::cout << "Current state: Shipped with tracking " << s.trackingNumber << std::endl;
            else std::cout << "Current state: Not Shipped" << std::endl;
        });

        // order.transitionToProcessing("P002"); // 抛出异常:Invalid transition to Processing.
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

通过std::variant,我们可以在编译期就明确一个订单可能处于哪些状态,并在运行时通过std::holds_alternativestd::visit安全地访问和处理不同状态下的数据和行为。

2.4 不可变性 (Immutability)

问题: 实体和值对象的状态如果可以随意修改,会导致以下问题:

  1. 并发问题: 在多线程环境下,可变对象容易引发数据竞争。
  2. 推理困难: 对象的生命周期内,其状态可能在任何时候被改变,使得理解和调试变得困难。
  3. 副作用: 意外的修改可能导致难以追踪的副作用。

解决方案: 优先考虑不可变性,特别是对于值对象。

  • const正确性: 尽可能使用const来标记不可修改的数据和方法。
  • 私有成员与构造函数初始化: 类的成员变量设为私有,并通过构造函数一次性初始化,不提供公共的setter方法。
  • 返回新实例: 对于需要“修改”操作的值对象,不修改自身,而是返回一个包含新状态的新实例。

示例:

// Money 值对象 (已在2.2中展示,这里再次强调其不可变性)
class Money {
public:
    enum class Currency { USD, EUR, GBP, CNY };
    Money(long long cents, Currency currency) : cents_(cents), currency_(currency) {
        if (cents_ < 0) throw std::invalid_argument("Amount cannot be negative.");
    }

    // 所有访问器都是 const 方法
    long long getCents() const { return cents_; }
    Currency getCurrency() const { return currency_; }

    // 运算方法返回新的Money实例,不修改自身
    Money add(const Money& other) const {
        if (currency_ != other.currency_) throw std::invalid_argument("Cannot add different currencies.");
        return Money(cents_ + other.cents_, currency_);
    }
    Money multiply(int multiplier) const {
        return Money(cents_ * multiplier, currency_);
    }

    bool operator==(const Money& other) const { return cents_ == other.cents_ && currency_ == other.currency_; }
    bool operator!=(const Money& other) const { return !(*this == other); }
    // ...
private:
    const long long cents_; // const 成员
    const Currency currency_; // const 成员
};

// Address 值对象
class Address {
public:
    Address(std::string street, std::string city, std::string postalCode)
        : street_(std::move(street)), city_(std::move(city)), postalCode_(std::move(postalCode)) {
        if (street_.empty() || city_.empty() || postalCode_.empty()) {
            throw std::invalid_argument("Address fields cannot be empty.");
        }
    }

    const std::string& getStreet() const { return street_; }
    const std::string& getCity() const { return city_; }
    const std::string& getPostalCode() const { return postalCode_; }

    // 没有setter方法
    // 如果需要“更改”地址,则创建一个新的Address对象
    Address withNewStreet(std::string newStreet) const {
        return Address(std::move(newStreet), city_, postalCode_);
    }

    bool operator==(const Address& other) const {
        return street_ == other.street_ && city_ == other.city_ && postalCode_ == other.postalCode_;
    }
    bool operator!=(const Address& other) const { return !(*this == other); }

private:
    const std::string street_;
    const std::string city_;
    const std::string postalCode_;
};

void test_immutability() {
    Address addr1("123 Main St", "Anytown", "12345");
    std::cout << "Addr1: " << addr1.getStreet() << std::endl;

    // 尝试修改 (编译错误或逻辑错误,取决于实现)
    // addr1.street_ = "456 New St"; // 编译错误,street_是const私有成员

    // 创建一个新地址来表示变化
    Address addr2 = addr1.withNewStreet("456 New St");
    std::cout << "Addr2: " << addr2.getStreet() << std::endl; // Output: 456 New St
    std::cout << "Addr1 (unchanged): " << addr1.getStreet() << std::endl; // Output: 123 Main St
}

不可变性使得值对象在传递和共享时更加安全,无需担心意外修改,从而简化了并发编程和程序推理。

2.5 编译期验证与策略 (Compile-time Validation & Policies)

问题: 某些业务约束可以在编译期被检查,而不是等到运行时才发现。例如,确保某个类型满足特定接口、数值在某个范围内等。运行时检查虽然重要,但编译期检查能更早地发现问题。

解决方案:

  • static_assert 在编译期断言某个条件。
  • C++20 Concepts: 明确模板参数需要满足的契约。
  • CRTP (Curiously Recurring Template Pattern): 实现通用接口或策略。

示例:static_assert

template<typename T, size_t MaxSize>
class BoundedString {
public:
    // 业务约束:字符串长度不能超过MaxSize
    static_assert(MaxSize > 0, "BoundedString MaxSize must be greater than 0.");

    explicit BoundedString(std::string s) : value_(std::move(s)) {
        if (value_.length() > MaxSize) {
            throw std::invalid_argument("String exceeds max size of " + std::to_string(MaxSize));
        }
    }

    const std::string& getValue() const { return value_; }

private:
    std::string value_;
};

void test_static_assert() {
    BoundedString<std::string, 10> short_str("hello"); // OK
    // BoundedString<std::string, 0> zero_size_str("test"); // 编译错误:MaxSize must be greater than 0.

    try {
        BoundedString<std::string, 5> too_long_str("world wide web"); // 运行时抛出异常
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

示例:C++20 Concepts (假设您使用C++20或更高版本)

Concepts 允许我们为模板参数定义清晰的语义要求,而不是依赖 SFINAE 的复杂性和隐式性。

#if __cplusplus >= 202002L // 仅在C++20及以上版本编译
#include <concepts>

// 定义一个概念:HasValidateMethod,要求类型T有一个无参数且返回bool的validate()方法
template<typename T>
concept HasValidateMethod = requires(T obj) {
    { obj.validate() } -> std::same_as<bool>;
};

// 定义一个需要验证的策略类
template<HasValidateMethod ValidatableType>
class ValidatorService {
public:
    bool validate(const ValidatableType& obj) const {
        return obj.validate();
    }
};

class ValidEmail {
public:
    ValidEmail(std::string email) : email_(std::move(email)) {}
    bool validate() const {
        // 实际邮箱验证逻辑
        return email_.find('@') != std::string::npos;
    }
private:
    std::string email_;
};

class NonValidatable {
public:
    void doSomething() {}
};

void test_concepts() {
    ValidatorService<ValidEmail> emailValidator;
    ValidEmail email("[email protected]");
    std::cout << "Email is valid: " << std::boolalpha << emailValidator.validate(email) << std::endl;

    // ValidatorService<NonValidatable> badValidator; // 编译错误!NonValidatable不满足HasValidateMethod概念
}
#endif

Concepts 极大地增强了模板的类型安全性和可读性,使得业务约束(如“这个类型必须是可验证的”)在编译期就能被清晰地表达和强制。

2.6 错误处理与领域异常 (Error Handling and Domain Exceptions)

问题: 业务逻辑中经常会遇到“非法”情况,例如无效的输入、不满足前置条件的调用。使用错误码或返回特殊值来表示这些情况,会导致:

  1. 代码冗余: 调用方需要不断检查返回值。
  2. 错误易被忽略: 忘记检查错误码会导致程序进入不一致状态。
  3. 缺乏上下文: 错误码通常不能提供足够的细节来诊断问题。

解决方案: 使用自定义的领域异常来表达业务规则的违反。

示例:

#include <stdexcept> // 包含标准的异常基类

// 定义一个领域异常的基类
class DomainException : public std::runtime_error {
public:
    explicit DomainException(const std::string& message) : std::runtime_error(message) {}
};

// 特定业务场景的异常
class InvalidQuantityException : public DomainException {
public:
    explicit InvalidQuantityException(int quantity)
        : DomainException("Invalid quantity: " + std::to_string(quantity) + ". Quantity must be positive.") {}
};

class InsufficientFundsException : public DomainException {
public:
    InsufficientFundsException(Money requested, Money available)
        : DomainException("Insufficient funds. Requested: " + std::to_string(requested.getCents()/100.0) +
                          ", Available: " + std::to_string(available.getCents()/100.0)) {}
};

class OrderCannotBeCancelledException : public DomainException {
public:
    explicit OrderCannotBeCancelledException(const Order::OrderId& orderId, Order::OrderStatus currentStatus)
        : DomainException("Order " + orderId.value + " cannot be cancelled in status " + std::to_string(static_cast<int>(currentStatus))) {}
};

// 假设有一个 Account 类
class Account {
public:
    Account(CustomerId customerId, Money initialBalance)
        : customerId_(std::move(customerId)), balance_(initialBalance) {}

    void debit(Money amount) {
        if (amount.getCurrency() != balance_.getCurrency()) {
            throw DomainException("Currency mismatch for debit operation.");
        }
        if (balance_.getCents() < amount.getCents()) {
            throw InsufficientFundsException(amount, balance_); // 抛出自定义异常
        }
        balance_ = balance_.add(amount.multiply(-1)); // 假设Money支持负数乘法或者有subtract方法
        std::cout << "Debited " << amount.getCents()/100.0 << ". New balance: " << balance_.getCents()/100.0 << std::endl;
    }

    Money getBalance() const { return balance_; }

private:
    CustomerId customerId_;
    Money balance_;
};

void test_domain_exceptions() {
    CustomerId custId("CUST-100");
    Account account(custId, Money(10000_usd)); // 初始100美元

    try {
        account.debit(2000_usd); // 扣款20美元
        account.debit(9000_usd); // 扣款90美元,余额不足
    } catch (const InsufficientFundsException& e) {
        std::cerr << "Caught InsufficientFundsException: " << e.what() << std::endl;
    } catch (const DomainException& e) {
        std::cerr << "Caught DomainException: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught general exception: " << e.what() << std::endl;
    }

    // Money mixed_currency_debit = 10.0_eur;
    // try {
    //     account.debit(mixed_currency_debit); // 抛出 Currency mismatch for debit operation.
    // } catch (const DomainException& e) {
    //     std::cerr << "Caught DomainException: " << e.what() << std::endl;
    // }
}

领域异常不仅明确表达了业务逻辑的失败原因,还提供了丰富的上下文信息,使得错误处理更加清晰和健壮。


3. C++类型系统与领域事件 (Domain Events)

领域事件是DDD中另一个重要概念,它表示在业务领域中发生的重要事情,例如“订单已支付”、“库存已更新”。领域事件本质上也是值对象,它们是不可变的,记录了事件发生时的所有相关信息。

在C++中表示领域事件:

  • 结构体/类: 定义一个表示事件的结构体或类,包含事件发生时的所有上下文数据。这些数据应该是不可变的。
  • 强类型: 每个事件都应该有自己独特的类型,这样事件处理器可以根据类型来订阅和处理。

示例:

#include <chrono> // 用于时间戳
#include <memory> // 用于智能指针

// 领域事件基类
class DomainEvent {
public:
    virtual ~DomainEvent() = default;
    std::chrono::system_clock::time_point occurredOn() const { return occurredOn_; }
protected:
    DomainEvent() : occurredOn_(std::chrono::system_clock::now()) {}
private:
    std::chrono::system_clock::time_point occurredOn_;
};

// 订单已支付事件
class OrderPaidEvent : public DomainEvent {
public:
    OrderPaidEvent(OrderId orderId, Money amount)
        : orderId_(std::move(orderId)), amountPaid_(amount) {}

    const OrderId& getOrderId() const { return orderId_; }
    const Money& getAmountPaid() const { return amountPaid_; }

private:
    OrderId orderId_;
    Money amountPaid_;
};

// 库存已更新事件
class InventoryUpdatedEvent : public DomainEvent {
public:
    InventoryUpdatedEvent(ProductId productId, Quantity newQuantity)
        : productId_(std::move(productId)), newQuantity_(newQuantity) {}

    const ProductId& getProductId() const { return productId_; }
    const Quantity& getNewQuantity() const { return newQuantity_; }

private:
    ProductId productId_;
    Quantity newQuantity_;
};

// 简单的事件发布器/订阅器(观察者模式)
class EventPublisher {
public:
    template<typename EventType>
    void subscribe(std::function<void(const EventType&)> handler) {
        // 使用 typeid 或其他机制来存储不同事件类型的处理器
        // 简化示例,实际会更复杂
        // For demonstration, we'll just store a generic handler for now
        // A real implementation would use std::map<std::type_index, std::vector<std::function<void(const DomainEvent&)>>>
        // and dynamic_cast or std::visit for dispatch.
        std::cout << "Subscribed to event type." << std::endl;
    }

    void publish(const DomainEvent& event) {
        // 实际的发布逻辑会根据事件类型分发给相应的处理器
        std::cout << "Published event at " << std::chrono::duration_cast<std::chrono::milliseconds>(event.occurredOn().time_since_epoch()).count() << "ms." << std::endl;
        // For example:
        // if (const auto* orderPaid = dynamic_cast<const OrderPaidEvent*>(&event)) {
        //    // call order paid handlers
        // } else if (const auto* inventoryUpdated = dynamic_cast<const InventoryUpdatedEvent*>(&event)) {
        //    // call inventory updated handlers
        // }
    }
};

void test_domain_events() {
    EventPublisher publisher;

    // 订阅 OrderPaidEvent
    publisher.subscribe<OrderPaidEvent>([](const OrderPaidEvent& event){
        std::cout << "Event Handler: Order " << event.getOrderId().value << " paid " << event.getAmountPaid().getCents()/100.0 << " USD." << std::endl;
    });

    // 发布一个 OrderPaidEvent
    OrderPaidEvent paidEvent(OrderId("ORD-003"), Money(15000_usd));
    publisher.publish(paidEvent);

    // 发布一个 InventoryUpdatedEvent
    InventoryUpdatedEvent inventoryEvent(ProductId("PROD-ABC"), Quantity(100));
    publisher.publish(inventoryEvent);
}

通过为每个领域事件定义强类型,我们确保了事件的语义清晰,并且在事件发布和订阅时能够进行类型检查,避免了处理错误类型事件的风险。


4. C++中的模块化与有界上下文 (Modularization and Bounded Contexts)

有界上下文是DDD中将大型复杂系统分解为更小、更易管理的部分的核心策略。每个有界上下文都有其自己的泛在语言、领域模型和团队。C++的模块化机制可以很好地支持有界上下文的实现。

  • 命名空间 (Namespaces): C++的命名空间是实现逻辑模块化的最直接方式。每个有界上下文可以拥有一个顶层命名空间。

    namespace OrderManagementContext {
        // Order聚合、LineItem、OrderStatus等都定义在这里
        class Order { /* ... */ };
        class OrderRepository { /* ... */ };
    }
    
    namespace InventoryContext {
        // Product聚合、StockItem、Warehouse等都定义在这里
        class Product { /* ... */ };
        class ProductRepository { /* ... */ };
    }
  • 物理分离 (Separate Compilation Units/Libraries): 更严格地,每个有界上下文可以编译成独立的库(静态库或动态库)。这强制了更强的解耦,一个上下文不能直接访问另一个上下文的内部实现细节,只能通过明确定义的接口进行交互。

    • 头文件与源文件: 头文件定义了上下文的公共API(契约),源文件包含其内部实现。
    • 模块 (C++20 Modules): C++20引入的模块特性为C++提供了更现代、更强大的模块化机制,它能够更好地封装实现细节,减少编译时间,并避免传统的头文件问题(如宏冲突、循环依赖)。这对于实现有界上下文的强封装性非常有益。
    // OrderManagementContext/order.ixx (Module interface unit)
    export module OrderManagement;
    
    import CoreTypes; // 导入共享的类型,如StrongId
    import InventoryContext; // 如果OrderManagement需要与InventoryContext交互,则导入其接口
    
    namespace OrderManagementContext {
        export class Order { /* ... */ };
        export class OrderRepository { /* ... */ };
        // ...
    }
    
    // InventoryContext/product.ixx
    export module Inventory;
    
    import CoreTypes;
    
    namespace InventoryContext {
        export class Product { /* ... */ };
        export class ProductRepository { /* ... */ };
        // ...
    }

    通过模块,我们可以清晰地定义每个上下文的导出接口,并严格控制其依赖关系,从而在编译期强制有界上下文的边界。


5. 最佳实践与注意事项

虽然利用C++类型系统表达业务约束带来了巨大的好处,但也需要注意以下几点:

  • 过度设计 vs. 恰当抽象: 不要为了类型安全而引入不必要的复杂性。对于简单的、无业务含义的原始类型(如循环计数器),直接使用int是没问题的。关键在于识别那些承载业务语义和约束的原始类型。
  • 性能考量: 创建大量小对象(值对象)可能带来轻微的内存开销和复制成本。现代C++编译器通常能很好地优化这些,但对于性能敏感的路径,需要进行分析和权衡。
  • 异常开销: 异常处理有运行时开销。对于预期内的、高频发生的“失败”情况(例如,验证用户输入),有时返回std::optional<T>std::expected<T, Error>可能更合适,而不是抛出异常。异常应保留给真正的“异常”情况。
  • 工具支持: 现代IDE(如CLion, Visual Studio Code with C++ extensions)提供了强大的类型检查、代码补全和重构功能,可以大大提高使用复杂类型系统的开发效率。
  • 团队协作: 统一的编码规范和对DDD原则的共同理解至关重要。确保团队成员都理解这些类型设计的意图。
  • 可序列化性: 如果值对象和实体需要序列化(例如,存储到数据库或通过网络传输),需要确保它们具备相应的序列化/反序列化机制。

通过本次讲座,我们深入探讨了如何将领域驱动设计的核心思想与C++强大的静态类型系统相结合。我们看到了C++如何超越其作为一门“系统编程语言”的传统认知,成为在复杂业务逻辑中表达和强制业务约束的强大工具。从强类型ID到不可变值对象,从状态机到领域异常,C++的类型系统为我们提供了丰富的手段,将业务规则直接编码到代码结构中,使得非法状态在编译期就无处遁形,从而构建出更健壮、更可靠、更符合领域模型的软件系统。这种实践不仅提升了代码质量,也促进了团队对业务领域的深入理解,最终交付更高价值的软件产品。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注