欢迎来到本次关于“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): 描述事物的特征,没有唯一标识,完全由其属性值定义。当属性值相同时,两个值对象被认为是相同的。它们应该是不可变的。例如,
Money、Address、Quantity。在C++中,值对象通常通过struct或class实现,其所有成员都是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;这样的定义。这种“原始类型痴迷”导致了几个问题:
- 类型不安全:
userId和productId都是std::string,编译器无法区分它们,可能会意外地将一个产品的ID赋给用户ID。 - 缺乏上下文: 仅仅看到
std::string,我们不知道它代表什么,是名称、描述还是ID? - 无法附加行为: 无法为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) 为值对象
问题: 许多业务概念,如金额、数量、百分比、邮箱地址等,常常被简单地用double、int、std::string等原始类型表示。这导致:
- 丢失业务语义:
double price;无法区分是商品单价、总价还是成本。 - 验证逻辑分散: 验证金额是否为正、邮箱格式是否正确等逻辑会散布在代码库的各个角落,容易遗漏和重复。
- 操作不安全: 直接对原始类型进行算术运算可能不符合业务规则(例如,不同货币不能直接相加)。
解决方案: 将这些原始类型封装成具有明确业务含义和行为的值对象。
示例:
// 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)
问题: 许多业务实体都有明确的生命周期和状态转换规则。如果使用裸整数或字符串来表示状态,容易出现:
- 非法状态: 允许将实体设置为业务上不可能的状态。
- 非法转换: 允许实体从一个状态直接跳到另一个不被允许的状态。
- 可读性差:
if (status == 1)不如if (status == OrderStatus::Pending)清晰。
解决方案: 使用enum class来表示状态,并在实体的方法中封装状态转换逻辑。利用现代C++的std::variant和std::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::variant 和 std::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_alternative或std::visit安全地访问和处理不同状态下的数据和行为。
2.4 不可变性 (Immutability)
问题: 实体和值对象的状态如果可以随意修改,会导致以下问题:
- 并发问题: 在多线程环境下,可变对象容易引发数据竞争。
- 推理困难: 对象的生命周期内,其状态可能在任何时候被改变,使得理解和调试变得困难。
- 副作用: 意外的修改可能导致难以追踪的副作用。
解决方案: 优先考虑不可变性,特别是对于值对象。
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)
问题: 业务逻辑中经常会遇到“非法”情况,例如无效的输入、不满足前置条件的调用。使用错误码或返回特殊值来表示这些情况,会导致:
- 代码冗余: 调用方需要不断检查返回值。
- 错误易被忽略: 忘记检查错误码会导致程序进入不一致状态。
- 缺乏上下文: 错误码通常不能提供足够的细节来诊断问题。
解决方案: 使用自定义的领域异常来表达业务规则的违反。
示例:
#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++的类型系统为我们提供了丰富的手段,将业务规则直接编码到代码结构中,使得非法状态在编译期就无处遁形,从而构建出更健壮、更可靠、更符合领域模型的软件系统。这种实践不仅提升了代码质量,也促进了团队对业务领域的深入理解,最终交付更高价值的软件产品。