CPP-Summit-2020 学习:面向领域模型的C++实现模式
领域驱动设计(Domain Driven Design,简称 DDD)是一种将软件设计与复杂业务领域紧密结合的方法论,核心思想是“模型先行,业务驱动”,通过领域模型(Domain Model)来表达业务规则和逻辑。DDD 的关键点:在实际落地 DDD 时,开发团队通常会遇到以下技术挑战:实体有唯一标识,生命周期内可以变更属性,但身份不变。2. 值对象(Value Object)值对象无唯一标识,完
一、领域驱动设计(DDD)概述
领域驱动设计(Domain Driven Design,简称 DDD)是一种将软件设计与复杂业务领域紧密结合的方法论,核心思想是“模型先行,业务驱动”,通过领域模型(Domain Model)来表达业务规则和逻辑。
DDD 的关键点:
- 聚焦核心领域(Core Domain)
- 不把所有业务都当成平等的模块处理,而是找出业务的核心价值。
- 核心领域用领域模型(Domain Model)表达,其他部分可用通用子域(Generic Subdomain)或支持子域(Supporting Subdomain)处理。
- 协作与统一语言(Ubiquitous Language)
- 领域模型不仅是代码,更是业务专家和开发者的沟通工具。
- 所有团队成员使用同一语言(如类名、方法名、事件名)描述业务逻辑。
- 迭代式演进(Iterative Evolution)
- 领域模型不是一次性设计完成,而是随着业务理解不断演化。
- DDD 鼓励小步迭代、持续重构,减少复杂性。
二、领域模型落地面临的技术挑战
在实际落地 DDD 时,开发团队通常会遇到以下技术挑战:
- 模型与实现的偏差
- 领域模型是业务概念的抽象,而代码实现容易受技术约束(框架、数据库)影响。
- 解决办法:使用**聚合(Aggregate)**封装核心业务逻辑,保持模型纯净。
- 复杂性管理
- 大型业务系统中,子域多、边界复杂,容易导致模型膨胀或耦合。
- 解决办法:划分界限上下文(Bounded Context),明确每个上下文负责的领域逻辑。
- 持久化与性能矛盾
- 领域模型对象和数据库表结构不一定完全匹配,尤其在 ORM 映射中。
- 解决办法:使用 仓储模式(Repository Pattern) 隐藏数据访问细节。
- 事件驱动与一致性
- 跨上下文或微服务时,领域事件(Domain Event)传播与事务一致性难以保证。
- 解决办法:引入事件总线(Event Bus)和最终一致性(Eventual Consistency)策略。
- 技术团队和业务团队协同困难
- 团队理解领域模型差异大,容易出现模型不统一或功能设计偏差。
- 解决办法:持续研讨、共享 Ubiquitous Language,并结合原型或行为驱动开发(BDD)。
三、DDD 关键概念与示例代码(Java 风格)
1. 实体(Entity)
实体有唯一标识,生命周期内可以变更属性,但身份不变。
#include <iostream>
#include <vector>
#include <string>
#include <numeric> // std::accumulate
#include <memory> // std::shared_ptr
// ------------------------
// 值对象 OrderItem
// ------------------------
struct OrderItem {
std::string name;
double price;
int quantity;
OrderItem(const std::string& n, double p, int q) : name(n), price(p), quantity(q) {}
};
// ------------------------
// 实体 Order
// ------------------------
class Order {
private:
std::string orderId; // 唯一标识
std::string customerName;
std::vector<OrderItem> items;
public:
Order(const std::string& id, const std::string& customer)
: orderId(id), customerName(customer) {}
// 添加订单项
void addItem(const OrderItem& item) {
items.push_back(item);
}
// 计算订单总额
double calculateTotal() const {
// 数学公式:
// $Total = \sum_{i=1}^{n} item.price * item.quantity$
double total = 0.0;
for (const auto& item : items) {
total += item.price * item.quantity;
}
return total;
}
std::string getId() const { return orderId; }
};
2. 值对象(Value Object)
值对象无唯一标识,完全由属性定义。
#include <string>
// 值对象 Address
class Address {
private:
std::string street;
std::string city;
std::string zipCode;
public:
Address(const std::string& s, const std::string& c, const std::string& z)
: street(s), city(c), zipCode(z) {}
// 值对象相等性比较
bool operator==(const Address& other) const {
return street == other.street && city == other.city && zipCode == other.zipCode;
}
};
3. 聚合与仓储(Aggregate & Repository)
聚合根负责维护内部一致性,仓储负责持久化。
#include <unordered_map>
#include <memory>
#include <stdexcept>
// 仓储接口
class OrderRepository {
public:
virtual void save(const std::shared_ptr<Order>& order) = 0;
virtual std::shared_ptr<Order> findById(const std::string& orderId) = 0;
};
// 内存实现
class InMemoryOrderRepository : public OrderRepository {
private:
std::unordered_map<std::string, std::shared_ptr<Order>> storage;
public:
void save(const std::shared_ptr<Order>& order) override {
storage[order->getId()] = order;
}
std::shared_ptr<Order> findById(const std::string& orderId) override {
auto it = storage.find(orderId);
if (it != storage.end()) return it->second;
throw std::runtime_error("Order not found");
}
};
四、领域事件与公式示例
DDD 中常用领域事件(Domain Event)表示系统状态变化:
#include <iostream>
#include <chrono>
#include <ctime>
// 领域事件
class OrderPlacedEvent {
private:
std::string orderId;
std::time_t timestamp;
public:
OrderPlacedEvent(const std::string& id) : orderId(id) {
timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
}
void publish() const {
std::cout << "订单已下单: " << orderId
<< " 时间: " << std::ctime(×tamp);
}
};
测试示例
int main() {
// 创建订单
auto order = std::make_shared<Order>("1001", "张三");
// 添加订单项
order->addItem({"苹果", 3.5, 5});
order->addItem({"香蕉", 2.0, 10});
// 计算总额
std::cout << "订单总额: " << order->calculateTotal() << std::endl;
// 发布领域事件
OrderPlacedEvent event(order->getId());
event.publish();
// 仓储操作
InMemoryOrderRepository repo;
repo.save(order);
auto loadedOrder = repo.findById("1001");
std::cout << "从仓储读取订单总额: " << loadedOrder->calculateTotal() << std::endl;
return 0;
}
数学公式示例(订单总额):
Total=∑i=1n(pricei×quantityi) Total = \sum_{i=1}^{n} (price_i \times quantity_i) Total=i=1∑n(pricei×quantityi)
五、总结
领域驱动设计的核心价值:
- 将复杂业务抽象成领域模型。
- 聚焦核心领域,减少无关复杂性。
- 使用迭代式演进不断完善模型。
- 通过聚合、仓储、领域事件等技术手段,将模型落地到实际系统中。
落地 DDD 的技术挑战主要是:
- 模型与实现偏差
- 复杂性管理
- 持久化与性能矛盾
- 跨上下文一致性
- 团队协同困难
通过 界限上下文、聚合、仓储、领域事件 可以有效应对这些挑战。
领域建模示意图
<?xml version="1.0" encoding="UTF-8"?>一、SVG 图概述
这张 SVG 图展示了 领域建模(Domain Modeling) 中的核心流程和参与者:
- 参与者(Actors)
- Domain Expert(领域专家):业务知识最丰富,定义核心业务概念。
- Analyst(分析师):把业务需求成可实现的模型和文档。
- Developer(开发者):负责把领域模型落地到代码。
- 核心概念
- Ubiquitous Language(通用语言):领域专家、分析师、开发者共用的业务语言,用于减少理解偏差。
- Whiteboard Discussions(白板讨论):团队协作的具体手段,通过图形和流程讨论业务规则。
- 输出产物
- Application Code(应用代码):实现领域逻辑的生产代码。
- Test Code(测试代码):保证领域模型逻辑正确性。
- Specs and Documentation(文档与规格说明):确保团队对模型理解一致。
图中箭头表示信息流与反馈循环,例如从 领域专家 → 分析师 → 开发者 → 白板讨论 → 应用代码/测试/文档,形成 迭代式模型演进。
二、理解与落地注释
- Ubiquitous Language 的作用
“通用语言”让团队成员在讨论时使用相同术语。
代码实现中,每个类、方法、事件命名都应反映业务概念,而不是技术实现。
C++ 代码示例:
// 订单实体 Order 表示业务中的“Order”,与领域语言一致
class Order {
private:
std::string orderId; // 唯一标识
std::string customerName;
public:
Order(const std::string& id, const std::string& customer)
: orderId(id), customerName(customer) {}
};
- 领域专家到开发者的信息流
- SVG 图中的箭头表示 知识从领域专家 → 分析师 → 开发者传递。
- 开发者通过白板讨论把需求转化为代码。
示例注释:
// 白板讨论 -> 决定订单的业务规则
// 例如:Order 总额 = 所有订单项价格 * 数量
double calculateTotal(const std::vector<OrderItem>& items) {
// 数学公式:
// $Total = \sum_{i=1}^{n} price_i * quantity_i$
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity;
return total;
}
- 输出产物的关系
- Application Code → 实现业务逻辑
- Test Code → 验证业务逻辑正确性
- Documentation → 保证团队理解一致
这些产物形成闭环,帮助迭代优化领域模型。
// 仓储模式示例,隐藏持久化细节
class OrderRepository {
public:
virtual void save(const std::shared_ptr<Order>& order) = 0;
virtual std::shared_ptr<Order> findById(const std::string& orderId) = 0;
};
三、数学公式示例
在领域建模中,业务规则通常可以用公式表达,例如订单总额、折扣计算等。
- 订单总额(单行公式):
Total=∑i=1npricei×quantityiTotal = \sum_{i=1}^{n} price_i \times quantity_iTotal=∑i=1npricei×quantityi - 折扣后总额(多行公式):
DiscountedTotal=Total−(Total×DiscountRate) 其中 DiscountRate∈[0,1] \text{DiscountedTotal} = Total - (Total \times DiscountRate) \ \text{其中 } DiscountRate \in [0,1] DiscountedTotal=Total−(Total×DiscountRate) 其中 DiscountRate∈[0,1] - 税费计算:
FinalAmount=Total×(1+TaxRate) \text{FinalAmount} = Total \times (1 + TaxRate) FinalAmount=Total×(1+TaxRate)
这些公式可以直接映射到代码中:
double discountedTotal(double total, double discountRate) {
return total - (total * discountRate); // $DiscountedTotal = Total - Total * DiscountRate$
}
double finalAmount(double total, double taxRate) {
return total * (1 + taxRate); // $FinalAmount = Total * (1 + TaxRate)$
}
四、总结
这张 SVG 领域建模图传达了 DDD 落地的核心流程:
- 协作和统一语言:领域专家、分析师、开发者共同使用 Ubiquitous Language。
- 迭代式演进:通过白板讨论、测试和文档,不断完善领域模型。
- 产出闭环:Application Code、Test Code、Documentation,确保模型实现、验证和理解一致。
- 公式化业务规则:将核心业务逻辑数学化,便于在代码中实现和验证。
领域模型落地的困境
一、领域模型落地的困境概述
在领域驱动设计(DDD)中,领域模型是业务知识的抽象,但在实际落地到代码时,会遇几个典型问题:
1. 模型不能映射到代码 / 映射过于复杂
- 问题:如果领域模型抽象得过于理论化,开发者无法将其转化为可执行代码,或者映射关系晦涩难懂,那么:
- 模型的价值降低,不能保证软件正确性。
- 团队在实现时容易出错或偏离业务意图。
- 示例:
假设有一个复杂的订单折扣模型:
FinalPrice=∑i=1npricei×quantityi×f(discounti) FinalPrice = \sum_{i=1}^{n} price_i \times quantity_i \times f(discount_i) FinalPrice=i=1∑npricei×quantityi×f(discounti)
如果在代码中写成一大堆嵌套if-else或复杂公式,其他开发者很难理解。
C++ 可读实现示例(带注释):
struct OrderItem {
double price;
int quantity;
double discountRate; // 折扣率
};
// 使用函数明确模型逻辑
double calculateItemPrice(const OrderItem& item) {
// $ItemTotal = price * quantity * (1 - discountRate)$
return item.price * item.quantity * (1 - item.discountRate);
}
double calculateOrderTotal(const std::vector<OrderItem>& items) {
double total = 0.0;
for (const auto& item : items)
total += calculateItemPrice(item); // 保持公式可理解
return total;
}
通过把公式封装成函数,模型和代码映射清晰,避免晦涩。
2. 编写代码的人不理解或不负责模型
- 问题:如果开发者认为模型只是“文档”,或者不理解如何让模型在程序中发挥作用:
- 领域模型无法反映在软件行为上。
- 软件可能运行正常,但业务规则偏离模型意图。
- 示例:
// 错误示例:直接硬编码业务逻辑,没有体现模型
double calculateTotalHardcoded() {
return 100.0 + 200.0; // 忽略折扣、数量等模型规则
}
- 正确做法是 让模型成为代码的一部分:
class Order {
private:
std::vector<OrderItem> items;
public:
double calculateTotal() const {
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity * (1 - item.discountRate); // 体现模型
return total;
}
};
3. 代码改变会破坏模型
- 问题:如果开发人员在重构代码时没有意识到模型与代码的绑定关系:
- 改变代码可能破坏模型正确性。
- 模型失去约束力,软件行为偏离业务规则。
- 示例:
// 修改代码但忽略折扣模型
double Order::calculateTotal() const {
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity; // 忽略折扣Rate,模型被破坏
return total;
}
- 解决方法:
- 重构时保持模型逻辑完整
- 可以用 测试代码验证模型一致性
void testOrderTotal() {
Order order;
order.addItem({100.0, 2, 0.1}); // price=100, qty=2, discount=10%
double expected = 100.0 * 2 * (1 - 0.1); // $Total = price * quantity * (1 - discount)$
assert(order.calculateTotal() == expected);
}
使用测试保证模型与代码的一致性,即便重构也不会破坏模型。
四、总结(理解)
领域模型落地的主要困境:
- 模型难以映射到代码 → 解决方法:封装业务公式、明确函数和类。
- 开发者不理解或不负责模型 → 解决方法:让模型成为代码的一部分,并用统一语言命名。
- 重构可能破坏模型 → 解决方法:用测试验证模型一致性,确保代码改动不违背模型逻辑。
核心原则:
- 领域模型必须可执行 → 不能只是文档。
- 代码必须忠实实现模型 → 避免偏离业务逻辑。
- 重构要保持模型完整 → 用测试和约束保证一致性。
“实现领域驱动设计(DDD Implementation)”
一、实现领域模型的原则
- 重新阐释和发展 DDD
- DDD 不仅是理论,更是 模型驱动的软件实现方法。
- 核心目标:让 代码忠实反映业务模型,而不是仅仅是技术实现。
- 模型的可执行性
- 领域模型不是文档,而是 可执行的代码,能直接表达业务规则。
- 核心思想:模型就是代码,代码就是模型。
- 案例易于理解
- 用 具体业务案例(如订单管理)来演示 DDD 模型和代码的映射。
二、实现模式与 DDD
- 实现模式介于 设计模式 和 语言手册之间,帮助开发者把领域模型落地。
- 与设计模式区别:
- 设计模式关注对象组织和交互,是 高层设计决策。
- 实现模式关注 如何用语言实现模型,是低层可执行实践。
比如 “聚合根(Aggregate Root)+ 仓储(Repository)+ 领域事件(Domain Event)”就是 DDD 的经典实现模式。
三、C++ 代码示例:实现领域模型
下面用 订单系统 举例,展示 实体、值对象、聚合、仓储、领域事件 的完整实现。
1. 值对象(Value Object)示例
#include <string>
// 值对象 Address
class Address {
private:
std::string street;
std::string city;
std::string zipCode;
public:
Address(const std::string& s, const std::string& c, const std::string& z)
: street(s), city(c), zipCode(z) {}
// 值对象比较逻辑
bool operator==(const Address& other) const {
return street == other.street && city == other.city && zipCode == other.zipCode;
}
};
说明:
值对象无唯一标识,由属性定义,完全不可变,保证模型一致性。
2. 实体(Entity)示例
#include <vector>
#include <memory>
struct OrderItem {
std::string name;
double price;
int quantity;
double discountRate; // 折扣率
};
// 订单实体 Order
class Order {
private:
std::string orderId; // 唯一标识
std::string customerName;
std::vector<OrderItem> items;
public:
Order(const std::string& id, const std::string& customer)
: orderId(id), customerName(customer) {}
void addItem(const OrderItem& item) {
items.push_back(item);
}
// 订单总额计算公式
// $Total = \sum_{i=1}^{n} price_i * quantity_i * (1 - discount_i)$
double calculateTotal() const {
double total = 0.0;
for (const auto& item : items) {
total += item.price * item.quantity * (1 - item.discountRate);
}
return total;
}
std::string getId() const { return orderId; }
};
说明:
- 订单是实体(Entity),有唯一标识
orderId。 - 聚合内部逻辑(如总额计算)直接映射业务模型。
3. 聚合根与仓储(Aggregate & Repository)
#include <unordered_map>
#include <stdexcept>
class OrderRepository {
public:
virtual void save(const std::shared_ptr<Order>& order) = 0;
virtual std::shared_ptr<Order> findById(const std::string& orderId) = 0;
};
// 内存仓储实现
class InMemoryOrderRepository : public OrderRepository {
private:
std::unordered_map<std::string, std::shared_ptr<Order>> storage;
public:
void save(const std::shared_ptr<Order>& order) override {
storage[order->getId()] = order;
}
std::shared_ptr<Order> findById(const std::string& orderId) override {
auto it = storage.find(orderId);
if (it != storage.end()) return it->second;
throw std::runtime_error("Order not found");
}
};
说明:
- 仓储模式隐藏持久化细节,让领域模型代码 专注业务逻辑。
- 聚合根(Order)保证内部一致性,外部只能通过聚合根访问。
4. 领域事件(Domain Event)
#include <iostream>
#include <chrono>
#include <ctime>
class OrderPlacedEvent {
private:
std::string orderId;
std::time_t timestamp;
public:
OrderPlacedEvent(const std::string& id) : orderId(id) {
timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
}
void publish() const {
std::cout << "订单已下单: " << orderId
<< " 时间: " << std::ctime(×tamp);
}
};
说明:
- 领域事件表示业务状态变化,可触发异步操作或集成其他系统。
5. 测试和验证模型一致性
#include <cassert>
void testOrderTotal() {
Order order("1001", "张三");
order.addItem({"苹果", 2.0, 5, 0.1});
order.addItem({"香蕉", 1.0, 10, 0.2});
// $Total = 2*5*(1-0.1) + 1*10*(1-0.2) = 9 + 8 = 17$
assert(order.calculateTotal() == 17.0);
}
说明:
- 测试确保 代码行为与领域模型一致。
- 即使重构,测试保证模型不被破坏。
四、总结
- 实现领域模型就是 用代码忠实表达业务模型,不要仅依赖文档或 UML。
- 实现模式提供可执行实践:聚合根、仓储、领域事件、值对象。
- 公式化业务规则方便验证:
Total=∑i=1npricei×quantityi×(1−discounti) Total = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti) - 测试是模型落地的保证,确保开发者理解并遵守模型。
C++ 语言面向模型的实现模式
一、面向模型的实现模式概述
C++ 面向模型的实现模式(Model-Oriented Implementation Pattern)核心思想:
- 模型驱动实现
- 代码结构直接反映领域模型的结构与规则。
- 保证 模型与代码双向映射,即模型可以指导代码实现,代码也能反映模型状态。
- 语言层面特性
- 利用 C++ 的特性:
- 类和继承 → 表达实体和聚合关系
- 值对象(immutable) → 使用
const或struct - 智能指针 → 管理聚合根生命周期
- 模板 → 泛化可重用组件
- 利用 C++ 的特性:
- 实现模式组织方式
- 按 模型元素类别组织:实体、值对象、聚合、仓储、领域事件、服务。
- 每类元素都对应 C++ 类型/类/函数,并保持业务规则完整。
二、核心模式及 C++ 实现示例
1. 值对象(Value Object)模式
- 实现要点:
- 无唯一标识
- 不可变
- 重载相等运算符以便比较
#include <string>
struct Address {
const std::string street;
const std::string city;
const std::string zipCode;
Address(const std::string& s, const std::string& c, const std::string& z)
: street(s), city(c), zipCode(z) {}
bool operator==(const Address& other) const {
return street == other.street && city == other.city && zipCode == other.zipCode;
}
};
说明:
const确保值对象不可变- 模型规则直接映射到 C++ 类型
2. 实体(Entity)与聚合根(Aggregate Root)模式
- 实现要点:
- 实体有唯一标识
- 聚合根保证内部一致性
- 封装业务规则
#include <vector>
#include <memory>
#include <string>
// 订单项
struct OrderItem {
std::string name;
double price;
int quantity;
double discountRate; // 折扣率
};
// 聚合根:Order
class Order {
private:
std::string orderId;
std::vector<OrderItem> items;
public:
Order(const std::string& id) : orderId(id) {}
void addItem(const OrderItem& item) {
items.push_back(item);
}
double calculateTotal() const {
// $Total = \sum_{i=1}^{n} price_i * quantity_i * (1 - discount_i)$
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity * (1 - item.discountRate);
return total;
}
std::string getId() const { return orderId; }
};
说明:
- 聚合根保证内部状态一致
- 业务规则(总额计算)直接映射模型公式
3. 仓储(Repository)模式
- 实现要点:
- 隐藏数据存储
- 提供聚合根访问接口
#include <unordered_map>
#include <stdexcept>
class OrderRepository {
public:
virtual void save(const std::shared_ptr<Order>& order) = 0;
virtual std::shared_ptr<Order> findById(const std::string& orderId) = 0;
};
// 内存仓储实现
class InMemoryOrderRepository : public OrderRepository {
private:
std::unordered_map<std::string, std::shared_ptr<Order>> storage;
public:
void save(const std::shared_ptr<Order>& order) override {
storage[order->getId()] = order;
}
std::shared_ptr<Order> findById(const std::string& orderId) override {
auto it = storage.find(orderId);
if (it != storage.end()) return it->second;
throw std::runtime_error("Order not found");
}
};
说明:
- 仓储是模型到代码的桥梁
- 聚合内部逻辑完全不依赖持久化实现
4. 领域事件(Domain Event)模式
#include <iostream>
#include <chrono>
#include <ctime>
class OrderPlacedEvent {
private:
std::string orderId;
std::time_t timestamp;
public:
OrderPlacedEvent(const std::string& id) : orderId(id) {
timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
}
void publish() const {
std::cout << "订单已下单: " << orderId
<< " 时间: " << std::ctime(×tamp);
}
};
说明:
- 模型状态变化可以生成事件
- 事件封装模型信息,便于异步处理或集成
5. 服务(Domain Service)模式
- 处理跨聚合逻辑或复杂业务规则
class DiscountService {
public:
// 计算订单折扣总额
static double calculateDiscount(const Order& order) {
double totalDiscount = 0.0;
for (const auto& item : order.getItems()) {
totalDiscount += item.price * item.quantity * item.discountRate;
}
// $TotalDiscount = \sum_{i=1}^{n} price_i * quantity_i * discount_i$
return totalDiscount;
}
};
说明:
- 服务处理模型之外的业务逻辑
- 保持聚合根单一职责
三、从模型到代码的双向映射最佳实践
- 模型公式 → 函数/方法
- 每个业务公式在 C++ 中有对应函数
- 示例:总额公式
Total=∑i=1npricei×quantityi×(1−discounti) Total = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti)
- 模型关系 → 类/聚合
- 聚合根管理内部实体和值对象
- 外部只能通过聚合根访问,保证一致性
- 模型状态变化 → 领域事件
- 每次重要业务状态变化产生事件
- 用事件驱动保持模型与外部系统同步
- 模型验证 → 测试代码
- 单元测试确保模型逻辑正确性
- 避免重构破坏模型
四、总结(理解)
- 面向 C++ 的模型实现模式,把 模型映射到可执行代码,保持业务规则清晰。
- 核心模式:
- 值对象(Value Object) → 不可变、可比较
- 实体/聚合根(Entity/Aggregate Root) → 唯一标识、封装业务规则
- 仓储(Repository) → 隐藏数据存储
- 领域事件(Domain Event) → 模型状态变化
- 服务(Domain Service) → 处理跨聚合或复杂业务逻辑
- 公式映射原则:
Total=∑i=1npricei×quantityi×(1−discounti) \text{Total} = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti) - 测试确保模型落地与业务一致。
mermaid
flowchart LR
A[C++面向模型的实现模式] -->| | B{ }
B --> C[表达概念与关系]
B --> D[生命周期管理]
B --> E[物理设计]
C --> F[表达概念]
C --> G[表达关系]
style A fill:#c0392b,stroke:#000,stroke-width:2px,color:#fff,font-weight:bold
style C fill:#2980b9,stroke:#000,stroke-width:2px,color:#fff
style D fill:#27ae60,stroke:#000,stroke-width:2px,color:#fff
style E fill:#7f8c8d,stroke:#000,stroke-width:2px,color:#fff
style F fill:#fff,stroke:#000
style G fill:#fff,stroke:#000
mermaid
mindmap
root((C++面向模型的<br>实现模式))
["表达概念"]
表达概念
表达关系
["生命周期管理"]
["物理设计"]
一、流程图理解
1. 核心节点解析
- A: C++ 面向模型的实现模式
- 核心主题,指 如何在 C++ 中把领域模型转化为可执行代码。
- B: 模型实现的三个维度
- 表达概念与关系(C):用 C++ 类、结构体和方法表达业务实体、值对象和聚合关系。
- 生命周期管理(D):管理对象的创建、销毁、所有权和聚合根的生命周期,C++ 可用智能指针(
std::shared_ptr/std::unique_ptr)实现。 - 物理设计(E):数据库、存储映射和性能优化,保证模型落地后的执行效率。
- C → F: 表达概念
- 用类/结构体表达业务概念,如订单(Order)、客户(Customer)、订单项(OrderItem)。
- C → G: 表达关系
- 表达实体之间的关系,例如聚合根包含子实体,订单包含多个订单项。
二、C++ 面向模型的实现示例
1. 表达概念(F)
struct OrderItem {
std::string name;
double price;
int quantity;
double discountRate; // 折扣率
};
class Order { // 聚合根
private:
std::string orderId;
std::vector<OrderItem> items;
public:
Order(const std::string& id) : orderId(id) {}
void addItem(const OrderItem& item) { items.push_back(item); }
double calculateTotal() const {
// $Total = \sum_{i=1}^{n} price_i * quantity_i * (1 - discount_i)$
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity * (1 - item.discountRate);
return total;
}
};
注释:
Order和OrderItem对应业务概念calculateTotal公式直接映射业务规则
2. 表达关系(G)
class Customer {
private:
std::string customerId;
std::string name;
std::vector<std::shared_ptr<Order>> orders; // 订单集合
public:
void addOrder(const std::shared_ptr<Order>& order) {
orders.push_back(order);
}
double totalSpent() const {
double total = 0.0;
for (const auto& order : orders)
total += order->calculateTotal(); // 订单总额累加
return total;
}
};
注释:
Customer与Order是 聚合关系- C++ 用
std::shared_ptr管理生命周期
3. 生命周期管理(D)
#include <memory>
// 聚合根生命周期由智能指针管理
auto order = std::make_shared<Order>("1001");
auto customer = std::make_shared<Customer>();
customer->addOrder(order); // 生命周期安全
说明:
- C++ 通过
std::shared_ptr保证聚合内部对象不会悬空 - 生命周期管理是模型落地的关键
4. 物理设计(E)
#include <unordered_map>
class OrderRepository {
private:
std::unordered_map<std::string, std::shared_ptr<Order>> storage;
public:
void save(const std::shared_ptr<Order>& order) {
storage[order->getId()] = order;
}
std::shared_ptr<Order> findById(const std::string& id) {
return storage.at(id);
}
};
说明:
- 仓储模式隐藏物理存储
- 使模型代码专注于业务逻辑,而不是数据库操作
三、数学公式在模型中的应用
- 订单总额公式:
Total=∑i=1npricei×quantityi×(1−discounti) Total = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti) - 客户总消费公式:
CustomerTotal=∑j=1mOrderTotalj CustomerTotal = \sum_{j=1}^{m} OrderTotal_j CustomerTotal=j=1∑mOrderTotalj
在 C++ 中直接通过循环或
std::accumulate实现。
double total = std::accumulate(customer->orders.begin(), customer->orders.end(), 0.0,
[](double sum, const std::shared_ptr<Order>& order){ return sum + order->calculateTotal(); });
四、总结
- C++ 面向模型的实现模式强调 模型到代码的双向映射。
- 核心维度:
- 表达概念与关系(C → F/G):类、结构体、聚合关系
- 生命周期管理(D):智能指针、聚合根管理对象
- 物理设计(E):仓储模式、数据存储映射
- 业务公式在代码中直接实现,保证模型落地的准确性。
- 模式组织方式使代码结构清晰、易于维护,并保持业务规则一致性。
mermaid
mindmap
root((Domain Model))
Mind
Document
UML
Code
Communication
Domain Model 思维导图 Mermaid 图
一、思维导图理解
1. 根节点:Domain Model(领域模型)
- 领域模型是对业务问题的抽象与概念化,用于指导软件开发。
- 它是 DDD(领域驱动设计)的核心,直接影响代码设计、文档、沟通方式。
2. 子节点解析
Mind(思维/认知)
- 模型首先是 概念化思维
- 团队需要理解业务规则和概念,形成统一认知
- 关键目标:确保模型表达的概念与业务一致
示例:用 C++ 类名直接体现业务概念
struct OrderItem {
std::string name; // 业务概念:商品名称
double price; // 业务概念:单价
int quantity; // 业务概念:数量
};
Document(文档)
- 文档是 模型的文字化记录
- 用于团队沟通、需求确认、测试基准
示例:文档描述订单总额公式
Total=∑i=1npricei×quantityi×(1−discounti) Total = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti) - 可以在 C++ 注释中直接体现公式:
// Order.calculateTotal()
// $Total = \sum_{i=1}^{n} price_i * quantity_i * (1 - discount_i)$
double calculateTotal(const std::vector<OrderItem>& items) {
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity * (1 - item.discountRate);
return total;
}
UML(统一建模语言)
- UML 是 图形化表达模型的工具
- 类图、聚合关系图、序列图都是 UML 的典型用法
- 在 C++ 中可以直接映射 UML 类图到类、聚合、方法
示例 UML → C++ 映射:
Order --- contains ---> OrderItem
OrderItem: name, price, quantity
对应 C++ 代码:
class Order {
private:
std::vector<OrderItem> items; // 聚合关系
};
Code(代码)
- 代码是模型的最终落地
- 模型公式、聚合、实体关系都要在代码中实现
class Order {
public:
double calculateTotal() const {
double total = 0.0;
for (const auto& item : items)
total += item.price * item.quantity * (1 - item.discountRate);
return total;
}
};
Communication(沟通)
- 沟通保证模型 被团队理解并一致执行
- 可以通过:
- 白板讨论(Whiteboard Discussion)
- UML 图、文档
- 代码审查和单元测试
示例:团队沟通业务规则
// 确认业务:每个订单项折扣率必须在 [0, 1]
assert(item.discountRate >= 0.0 && item.discountRate <= 1.0);
二、总结:Domain Model 的五个层面
| 层面 | 理解 | C++ 实现 |
|---|---|---|
| Mind | 概念化思维,理解业务概念 | 类、结构体、枚举 |
| Document | 文档记录公式和规则 | 注释 + 文档 |
| UML | 图形化表达概念与关系 | 类图 → C++ 类、聚合 |
| Code | 模型落地 | 聚合根、实体、方法、公式实现 |
| Communication | 团队理解和一致性 | 白板、审查、断言/测试 |
核心原则:
- 公式映射保证业务逻辑清晰:
Total=∑i=1npricei×quantityi×(1−discounti) Total = \sum_{i=1}^{n} price_i \times quantity_i \times (1 - discount_i) Total=i=1∑npricei×quantityi×(1−discounti) - 聚合关系和实体映射模型结构
- 文档 + UML + 代码 + 沟通形成闭环,保证模型落地不偏离业务意图
C++ 实现领域模型的“表达概念”模式
一、概念解析:表达概念(Express Concept)
在 C++ 面向模型的实现模式中,表达概念是指如何将领域模型中的业务概念准确地映射到代码中,使代码不仅可执行,同时清晰地反映业务意义。
1. 命名(Naming)
- 类、方法、属性名称要兼顾:
- 领域语义:反映业务概念
- 例如
Port、Switch、Connection等名称清楚表示网络领域概念
- 例如
- 模型角色:说明在模型中的职责
Port是聚合根还是实体?属性bandwidth是值对象?
- 领域语义:反映业务概念
2. 实现选型(Implementation Choice)
- 类(class) → 优先用于复杂业务实体
- POD(Plain Old Data)结构体 → 用于简单值对象
- 基本类型 → 用于属性或小型值对象
示例:
struct Bandwidth { // 值对象
double value; // 带宽值
std::string unit; // 单位 Mbps/Gbps
};
3. 构造与析构(Constructor & Destructor)
- 构造函数服务于 生命周期管理
- 声明依赖关系,如
Port依赖Switch
class Switch; // 前向声明
class Port {
private:
int id;
Switch* parentSwitch; // Port 依赖的 Switch
public:
Port(int id, Switch* s) : id(id), parentSwitch(s) {}
~Port() { releaseAllConnections(); }
void releaseAllConnections() {
// 清理逻辑,释放依赖资源
}
};
4. 行为(Behavior)
- 对外接口(public method)先于对内复用
- 对外接口体现业务动作,例如端口连接、释放连接
class Connection {};
class PeerNode {}; // 对等节点
class Port {
public:
Connection connect(PeerNode* peer) {
// 建立连接逻辑
Connection conn;
return conn;
}
void release(const Connection& conn) {
// 断开连接逻辑
}
};
5. 属性(Attribute)
- 区分“我是谁”(Identity)
- 体现依赖关系(Parent、聚合等)
class Port {
private:
int id; // 我是谁,唯一标识
double bandwidth; // 属性
Switch* parent; // 所属聚合
};
6. 物理结构(File & Organization)
- 每个概念单独文件
- 命名一致,格式统一
src/
└─ domain/
├─ Port.h
├─ Port.cpp
├─ Switch.h
└─ Switch.cpp
- 便于理解模型角色,保持代码可维护性
二、Mermaid 类图解析
解析
| 成员 | 角色 | 理解 |
|---|---|---|
id : PortId |
私有属性 | 唯一标识,体现“我是谁” |
bandwidth : Bandwidth |
私有属性 | 值对象,表达带宽概念 |
Port(id, s) |
构造函数 | 声明依赖关系(Port 属于 Switch) |
connect(peer) |
公共方法 | 对外行为,建立连接 |
release(conn) |
公共方法 | 对外行为,释放连接 |
三、公式化示例(Bandwidth计算)
假设端口带宽计算公式:
EffectiveBandwidth=bandwidth×(1−utilization) EffectiveBandwidth = bandwidth \times (1 - utilization) EffectiveBandwidth=bandwidth×(1−utilization)
在 C++ 中:
class Port {
private:
double bandwidth; // Mbps
double utilization; // 0.0 ~ 1.0
public:
double effectiveBandwidth() const {
// $EffectiveBandwidth = bandwidth * (1 - utilization)$
return bandwidth * (1 - utilization);
}
};
四、总结:表达概念的实现模式要点
- 命名 → 清晰表达领域概念和模型角色
- 实现选型 → 类 > POD > 基本类型
- 构造与析构 → 管理生命周期,声明依赖关系
- 行为 → 公共接口优先体现业务动作
- 属性 → 表示身份和依赖
- 物理结构 → 文件独立、命名统一
通过这种模式,C++ 代码直接反映领域模型概念,便于维护和理解,同时保证业务规则和对象关系在代码中清晰表达。
C++ 领域模型“表达概念:命名”类图
一、命名原则解析
在领域模型实现中,命名需要兼顾 清晰简洁、领域语义、模型角色:
- 领域语义(Business Meaning)
- 类名能直接反映业务概念
- 例子:
Port→ 网络端口PortId→ 端口唯一标识Bandwidth→ 带宽属性Switch→ 交换机
- 模型角色(Model Role)
- 通过后缀或命名约定体现角色
- 常见模式:
xxService→ 提供业务操作,例如ConnectionSetupServicexxEvent→ 领域事件,例如ConnectionSetupEventxxFactory→ 工厂模式创建实体,例如SwitchFactoryxxRepository→ 仓储模式管理聚合,例如SwitchRepositoryxxContext→ 上下文对象,例如ContextxxRole→ 角色对象或接口(本例未体现)
命名原则核心:看到类名即理解业务概念 + 模型角色,无需额外查阅说明文档。
二、类图结构解析
关系理解
2. 类之间的关系解析
| 类关系 | 理解 | C++ 对应实现 |
|---|---|---|
ContextInitService ..> Context : create |
初始化服务创建上下文 | 构造函数或工厂方法返回 Context 对象 |
ContextInitService --> SwitchRepository |
依赖 Switch 仓储 | ContextInitService 持有 SwitchRepository* |
ContextInitService --> SwitchFactory |
依赖工厂 | ContextInitService 调用 SwitchFactory->create() |
ConnectionSetupService o-- Context |
聚合 Context | 持有 Context& 或智能指针 |
ConnectionSetupService --> ConnectionSetupEvent |
生成事件 | 调用事件构造函数 |
ConnectionSetupService --> Port |
操作端口 | 调用 Port 方法 |
SwitchFactory ..> Switch : create |
工厂创建 Switch | SwitchFactory::createSwitch() 返回 Switch 对象 |
SwitchRepository o-- "1 .. *" Switch |
仓储聚合多个 Switch | std::vector<std::shared_ptr<Switch>> |
Switch o-- "1 .. N" Port |
Switch 包含多个 Port | 聚合关系,成员变量 std::vector<Port> |
Port o-- PortId |
Port 拥有唯一标识 | PortId 值对象成员 |
Port o-- Bandwidth |
Port 拥有带宽属性 | Bandwidth 值对象成员 |
| 类关系 | 解析 |
|---|---|
ContextInitService ..> Context : create |
初始化服务创建上下文对象 |
ContextInitService --> SwitchRepository |
初始化服务依赖 Switch 仓储 |
ContextInitService --> SwitchFactory |
初始化服务依赖 Switch 工厂 |
ConnectionSetupService o-- Context |
连接建立服务聚合 Context |
ConnectionSetupService --> ConnectionSetupEvent |
连接建立时生成事件 |
ConnectionSetupService --> Port |
连接建立服务操作 Port 实体 |
SwitchFactory ..> Switch : create |
工厂创建 Switch 实体 |
SwitchRepository o-- "1 .. *" Switch |
仓储聚合多个 Switch |
Switch o-- "1 .. N" Port |
Switch 聚合多个 Port |
Port o-- PortId |
Port 拥有唯一标识 |
Port o-- Bandwidth |
Port 拥有带宽属性 |
三、C++ 实现示例
1. 值对象:PortId 和 Bandwidth
struct PortId {
int id; // 唯一标识
PortId(int i) : id(i) {}
};
struct Bandwidth {
double value; // Mbps
Bandwidth(double v) : value(v) {}
};
2. 实体:Port
class Port {
private:
PortId portId; // 身份标识
Bandwidth bandwidth; // 带宽属性
public:
Port(const PortId& id, const Bandwidth& bw)
: portId(id), bandwidth(bw) {}
double effectiveBandwidth(double utilization) const {
// 公式:有效带宽 = 总带宽 * (1 - 利用率)
// $EffectiveBandwidth = Bandwidth \times (1 - Utilization)$
return bandwidth.value * (1 - utilization);
}
};
3. 聚合:Switch
#include <vector>
#include <memory>
class Switch {
private:
std::vector<std::shared_ptr<Port>> ports; // 聚合多个 Port
public:
void addPort(const std::shared_ptr<Port>& port) {
ports.push_back(port);
}
size_t portCount() const { return ports.size(); }
};
4. 仓储与工厂
#include <unordered_map>
#include <string>
class SwitchRepository {
private:
std::unordered_map<std::string, std::shared_ptr<Switch>> storage;
public:
void save(const std::string& id, std::shared_ptr<Switch> sw) {
storage[id] = sw;
}
std::shared_ptr<Switch> findById(const std::string& id) {
return storage.at(id);
}
};
class SwitchFactory {
public:
std::shared_ptr<Switch> createSwitch() {
return std::make_shared<Switch>();
}
};
5. 服务:ContextInitService & ConnectionSetupService
class Context {};
class ContextInitService {
private:
SwitchRepository* repo;
SwitchFactory* factory;
public:
ContextInitService(SwitchRepository* r, SwitchFactory* f)
: repo(r), factory(f) {}
std::shared_ptr<Context> createContext() {
auto ctx = std::make_shared<Context>();
auto sw = factory->createSwitch();
repo->save("SW1", sw);
return ctx;
}
};
class ConnectionSetupEvent {};
class ConnectionSetupService {
private:
std::shared_ptr<Context> context;
public:
ConnectionSetupService(std::shared_ptr<Context> ctx) : context(ctx) {}
void setupConnection(Port& port) {
ConnectionSetupEvent event; // 生成领域事件
// 连接建立逻辑
}
};
四、命名与模型角色总结
| 类名 | 领域语义 | 模型角色 |
|---|---|---|
Port |
网络端口 | 实体 |
PortId |
端口唯一标识 | 值对象 |
Bandwidth |
带宽属性 | 值对象 |
Switch |
交换机 | 聚合根 |
SwitchFactory |
创建交换机 | 工厂 |
SwitchRepository |
保存与查询交换机 | 仓储 |
Context |
上下文 | 聚合对象 |
ContextInitService |
初始化上下文 | 服务 |
ConnectionSetupService |
建立端口连接 | 服务 |
ConnectionSetupEvent |
连接事件 | 事件 |
命名原则:清晰、简洁、直接表达业务语义 + 模型角色,让团队无需额外说明就能理解对象职责。
五、公式映射示例
- 端口有效带宽公式:
EffectiveBandwidth=Bandwidth×(1−Utilization) EffectiveBandwidth = Bandwidth \times (1 - Utilization) EffectiveBandwidth=Bandwidth×(1−Utilization) - 交换机总有效带宽:
TotalSwitchBandwidth=∑i=1NEffectiveBandwidthi TotalSwitchBandwidth = \sum_{i=1}^{N} EffectiveBandwidth_i TotalSwitchBandwidth=i=1∑NEffectiveBandwidthi
在 C++ 中通过方法实现:
double totalBandwidth() const {
double total = 0.0;
for (const auto& port : ports)
total += port->effectiveBandwidth(0.1); // 假设利用率 10%
return total;
}
✓ 总结:
- 类名表达 领域语义(Port、Switch、Bandwidth)
- 后缀表达 模型角色(Service、Event、Factory、Repository、Context)
- 代码结构反映领域聚合关系、生命周期管理和业务公式
- 保证模型落地的清晰性、可维护性和业务一致性
C++ 领域模型“Port、PortId、Bandwidth”类图与实现片段
一、类图理解
解析
- Port
- 领域实体,表示网络端口
- 聚合了
PortId(身份标识)和Bandwidth(带宽属性)
- PortId
- 值对象,表示端口的唯一标识
- 在实现中用
using PortId = int;简化
- Bandwidth
- 值对象,表示端口带宽
- 封装带宽值,并提供相关方法(如扩展因子
getExtendFactor()) - 聚合在 Port 中
聚合关系用
o--表示,Port 拥有 PortId 和 Bandwidth。
二、PortId.h 实现
using PortId = int; // 端口唯一标识,值对象类型别名
注释
- 轻量值对象,直接用基本类型
int表示 - 简洁且符合领域语义
- 不包含行为,仅用于身份识别
三、Bandwidth.h 实现
#include "cub/base/EqHelper.h" // 辅助比较等号操作
class Bandwidth {
public:
explicit Bandwidth(unsigned int value)
: value(value) {
// 构造函数,初始化带宽值
}
DECL_EQUALS(Bandwidth); // 宏定义,生成 == 和 != 运算符,便于值对象比较
int getExtendFactor() const {
// 返回扩展因子(可能用于容量计算或加权带宽)
return value / 10; // 示例:每10 Mbps为一个单位
}
private:
const unsigned int value; // 带宽值,单位 Mbps,常量保证不可修改
};
理解
- 构造函数
explicit防止隐式类型转换value是带宽值
- 比较操作
DECL_EQUALS(Bandwidth)宏生成等值操作,保证值对象语义- 值对象通过值判断相等,而不是通过指针/地址
- 方法 getExtendFactor()
- 提供行为接口,计算扩展因子
- 公式示例:
ExtendFactor=Bandwidth10 ExtendFactor = \frac{Bandwidth}{10} ExtendFactor=10Bandwidth
- 私有属性
const unsigned int value确保不可修改,实现不可变对象(Immutable Object)- 遵循领域驱动设计中值对象“不可变”的原则
四、Port 实体示例(聚合 PortId 和 Bandwidth)
#include <memory>
#include "PortId.h"
#include "Bandwidth.h"
class Port {
private:
PortId portId; // 身份标识
Bandwidth bandwidth; // 带宽属性
public:
Port(PortId id, const Bandwidth& bw)
: portId(id), bandwidth(bw) {}
PortId getId() const { return portId; }
double getEffectiveBandwidth(double utilization) const {
// $EffectiveBandwidth = Bandwidth \times (1 - Utilization)$
return bandwidth.getExtendFactor() * (1 - utilization);
}
};
理解
- 聚合关系
- Port 聚合 PortId 和 Bandwidth
- 表示实体拥有唯一标识和带宽属性
- 方法 getEffectiveBandwidth
- 根据利用率计算有效带宽
- 将值对象行为与实体行为组合
- 不可变性与封装
Bandwidth内部值不可修改- Port 的核心属性通过构造函数注入
五、总结
| 类 | 类型 | 解释 |
|---|---|---|
| Port | 实体 | 网络端口,聚合 PortId 和 Bandwidth |
| PortId | 值对象 | 端口唯一标识,轻量型别别名 int |
| Bandwidth | 值对象 | 带宽值,封装不可变属性和行为接口 |
实现原则:
- 命名清晰简洁:Port、PortId、Bandwidth 一眼能看出领域语义
- 聚合关系明确:Port 聚合 PortId 和 Bandwidth
- 值对象不可变:Bandwidth 内部值用 const 保证
- 行为接口映射业务公式:如有效带宽计算公式
✓ 公式示例: - 带宽扩展因子
ExtendFactor=Bandwidth10 ExtendFactor = \frac{Bandwidth}{10} ExtendFactor=10Bandwidth - 有效带宽
EffectiveBandwidth=ExtendFactor×(1−Utilization) EffectiveBandwidth = ExtendFactor \times (1 - Utilization) EffectiveBandwidth=ExtendFactor×(1−Utilization)
在 C++ 中对应getExtendFactor()和getEffectiveBandwidth()方法实现。
C++ 领域模型“表达概念:构造与析构”
一、类图解析
理解
- Switch 与 Port
- (
Port) 聚合多个端口 交换机 (Switch) - 1 对 N 聚合关系
- 聚合意味着 Port 的生命周期与 Switch 相关
- (
- Port 与属性
- Port 聚合 PortId 和 Bandwidth
- PortId 表示端口身份
- Bandwidth 表示带宽属性
- 生命周期和依赖
- 构造函数初始化端口时必须提供 Switch 的引用
- 避免生成无效状态对象
二、Port.h 解析与理解
#include "PortId.h"
#include "Bandwidth.h"
class Switch;
class PeerNode;
class Connection;
class Port {
public:
// 构造函数:初始化端口并声明对 Switch 的依赖
Port(PortId id, const Switch& s);
// 对外行为:建立连接
Connection* connect(const PeerNode& peer);
// 对外行为:释放连接
void release(const Connection* conn);
private:
// 内部方法:计算带宽扩展因子
void getBandExpandFactor() const;
private:
const Switch& switch_; // 声明生命周期依赖,端口绑定到 Switch
PortId id; // 端口唯一标识
Bandwidth bandwidth; // 带宽属性
};
1. 构造函数(Constructor)
Port(PortId id, const Switch& s)- 作用:
- 完整初始化端口对象
- 声明端口依赖
Switch(通过引用传递,表明端口生命周期不超过 Switch) - 避免生成状态无意义的对象(对象创建即合法状态)
关键点:
- 使用
const Switch& s:- 表示 引用参数,端口对象不拥有 Switch
- 生命周期早于端口,保证端口操作始终有效
- 使用
PortId:- 唯一标识端口
- 值对象类型,直接存储
- 使用
Bandwidth:- 封装带宽值和扩展方法
- 保持不可变性(const)
2. 析构函数(Destructor)
- 示例中未显式写析构函数,但可以隐式使用默认析构
- 对于资源管理(如
Connection*):- 可以在
release()或智能指针中管理内存 - 避免裸指针泄漏
- 可以在
3. 对外依赖和读写约束
| 成员 | 理解 | 生命周期/约束 |
|---|---|---|
const Switch& switch_ |
端口依赖的交换机 | 生命周期依赖外部对象,使用引用保证端口不可独立于 Switch 存在 |
PortId id |
唯一标识 | 值对象,创建即固定 |
Bandwidth bandwidth |
带宽属性 | 值对象,封装行为和不可变值 |
connect(const PeerNode& peer) |
建立连接 | 参数为引用,生命周期早于端口 |
release(const Connection* conn) |
释放连接 | 通过指针管理资源,释放外部对象 |
4. 内部方法示例
void Port::getBandExpandFactor() const {
// 计算带宽扩展因子
// 公式:$ExtendFactor = Bandwidth / 10$
int factor = bandwidth.getValue() / 10;
}
- 内部方法服务于业务行为,不对外暴露
- 使用 const 修饰,保证不会修改 Port 状态
三、生命周期管理总结
- 构造即完整初始化
- Port 创建时必须提供 Switch 引用和 PortId
- 保证对象一开始就有效
- 引用声明依赖
const Switch&表示端口依赖交换机- 生命周期由外部对象管理
- 不可变值对象
- PortId 和 Bandwidth 保持不可变性
- 保证业务状态一致
- 行为接口先于内部复用
- 对外方法如
connect、release提供业务行为 - 内部方法如
getBandExpandFactor服务于实现
- 对外方法如
四、公式示例
- 带宽扩展因子
ExtendFactor=Bandwidth10 ExtendFactor = \frac{Bandwidth}{10} ExtendFactor=10Bandwidth - 有效带宽
EffectiveBandwidth=Bandwidth×(1−Utilization) EffectiveBandwidth = Bandwidth \times (1 - Utilization) EffectiveBandwidth=Bandwidth×(1−Utilization)
这些公式可在 Port 内部方法或对外接口中实现,保持领域模型与代码一致。
五、C++ 生命周期实践要点
| 原则 | C++ 实现 |
|---|---|
| 构造完整 | 构造函数必须初始化所有成员变量 |
| 生命周期依赖 | 使用引用或智能指针声明依赖关系 |
| 不可变值对象 | PortId, Bandwidth 使用 const 或值对象保证不可变 |
| 对外行为接口 | connect/release 提供业务操作 |
| 内部方法 | getBandExpandFactor 提供辅助行为,不破坏对象状态 |
✓ 总结:
- 构造函数与析构函数服务于 对象生命周期管理
- 显式依赖声明 (
const Switch&) 确保对象状态有效 - 值对象保持不可变,内部方法服务于行为
- 代码结构与领域模型紧密映射
UML 五种关系:谁对谁
| UML 关系 | UML符号 | 方向理解 | 谁整体谁部分 | 口诀 | C++ 对应 | |
|---|---|---|---|---|---|---|
| 依赖 | ..>(虚线箭头) |
箭头从依赖方指向被依赖方 | 无整体/部分 | 谁用谁,箭头指向被用对象 | 方法参数或返回值依赖的对象 | |
| 关联 | -->(实线箭头) |
箭头从知道方指向被知道方 | 无整体/部分 | 谁知道谁,箭头指向被知道对象 | 成员指针、引用或智能指针 | |
| 聚合 | o--(空心菱形) |
菱形在整体端,箭头指向部分 | 菱形端是整体,另一端是部分 | 菱形在哪,哪是整体,箭头指向部分,部分可独立 | 成员对象/成员指针,生命周期可独立 | |
| 组合 | *--(实心菱形) |
菱形在整体端,箭头指向部分 | 菱形端是整体,另一端是部分 | 实心菱形整体掌控,部分依赖整体销毁 | 成员对象或智能指针,随整体销毁 | |
| 继承 | `< | –` | 箭头从子类指向父类 | 父类是整体,子类是特化/扩展 | 父类整体,子类继承 | class Child : public Parent |
箭头指向的是被动的对象
C++ 领域模型“表达概念:行为”
一、UML 类图分析
理解
- EventHandler
- 声明一个接口(纯虚类),定义事件处理的行为
- 仅提供对外 API,不包含实现细节
- ConnectionSetupService
- 实现 EventHandler 接口
- 聚合 Context,对 Context 有依赖,但生命周期独立(空心菱形
o--) - 对外提供业务相关行为接口,内部逻辑 private
- Context
- 聚合到 ConnectionSetupService
- 提供服务或状态信息
行为表达原则
- 最小暴露:尽量少的 public 方法
- 只读方法:使用
const修饰,遵循 CQRS - 领域含义接口:避免无意义的 get/set(好莱坞原则:别叫我,我来调用你)
- 接口隔离:通过纯接口暴露 API,子类覆写的虚函数可私有化
二、C++ 代码解析
1. 接口定义(EventHandler.h)
// ConnectionSetupEvent.h
DEF_INTERFACE(EventHandler)
{
ABSTRACT(Status handle(const ConnectionSetupEvent& event) const);
};
DEF_INTERFACE和ABSTRACT宏封装纯虚类定义handle方法:- 接受事件参数
ConnectionSetupEvent - 使用
const修饰,保证方法只读,不修改对象状态 - 返回状态
Status,可用于 CQRS 的命令与查询分离
- 接受事件参数
2. 服务实现(ConnectionSetupService.h)
#include "EventHandler.h"
class Context;
class ConnectionSetupService : public EventHandler
{
public:
// 构造函数:注入 Context,声明依赖但不拥有
ConnectionSetupService(const Context& ctxt)
: ctxt(ctxt) {}
private:
// 覆写接口方法,private化
OVERRIDE(Status handle(const ConnectionSetupEvent& event) const);
private:
const Context& ctxt; // 聚合 Context,生命周期独立
};
理解与注释
- 构造函数
- 注入
const Context& ctxt - 表明生命周期依赖 Context,但不拥有(符合聚合语义)
- 注入
- handle 方法
- 覆写接口方法,private 化
- 对外 API 通过 EventHandler 指针或引用访问
- 避免直接调用,实现好莱坞原则
- 成员变量
const Context& ctxt- 只读引用,遵循 CQRS(命令查询分离)
- 明确依赖,聚合关系
三、领域行为设计原则对应
| 原则 | C++ 实现 | 理解 |
|---|---|---|
| 最小暴露 | handle 方法 private |
尽量少的 public 方法,只暴露必要接口 |
| CQRS | const 修饰 handle |
查询方法不修改状态,命令方法单独设计 |
| 好莱坞原则 | 接口访问 API | 对外调用通过接口,不直接依赖具体实现 |
| 接口隔离 | EventHandler 纯接口 | 客户只依赖所需接口,子类实现可私有化方法 |
| 聚合 | const Context& ctxt |
声明依赖 Context,生命周期独立(空心菱形 o–) |
四、示例调用模式
const Context ctxt;
ConnectionSetupService service(ctxt);
// 对外调用通过接口
EventHandler* handler = &service;
Status s = handler->handle(event); // 调用私有化实现
- 外部只通过接口访问
- 内部实现私有化,最小暴露
六、总结
- 行为优先接口化
- 对外只暴露有意义的业务行为
- 避免 get/set,突出领域语义
- 聚合 Context
- 声明依赖,但不拥有
- 生命周期独立 → 空心菱形
o--
- 接口继承
- 接口隔离原则:客户只依赖接口
- 子类覆写虚函数可私有化,遵循好莱坞原则
- CQRS
- const 修饰只读方法
- 命令和查询分离
C++ 领域模型“表达概念:属性”
一、属性设计的核心思想
- 区分属性与依赖
- 属性(Attribute / Value):描述对象本身特性,例如 Port 的 id、Bandwidth。
- 依赖(Dependency / Reference):表示对象对外部实体的引用或关系,例如 Port 对 Switch 的依赖。
- 实体标识 vs 值对象
- 实体(Entity):具有唯一标识,“我是谁”,标识用 id 表示。
- 示例:
PortId id;
- 示例:
- 值对象(Value Object):无独立标识,值相同则相等,通常不可变。
- 示例:
Bandwidth bandwidth;
- 示例:
- 实体(Entity):具有唯一标识,“我是谁”,标识用 id 表示。
- 不可变设计
- 值对象尽量用
const修饰,避免修改,保证值语义。 - 比较方法(operator==)实现值比较,支持“我是谁”判断。
- 值对象尽量用
- 显示化依赖
- 值类型 → 完全拥有
- 指针/引用 → 依赖外部对象
- const 修饰 → 只读访问
二、Port.h 分析
#include "PortId.h"
#include "Bandwidth.h"
class Switch;
class Port {
public:
// 构造函数:声明依赖 Switch,初始化属性
Port(PortId id, const Switch& s);
private:
PortId id; // 实体标识,区分不同 Port
Bandwidth bandwidth; // 值对象,表示带宽属性,不可变
const Switch& host; // 显示依赖,端口绑定到 Switch,但生命周期独立
};
理解
- id → 实体标识,表达“我是谁”,Port 的唯一标识
- bandwidth → 值对象,保证不可变,通过
operator==比较值相等性 - host → 引用外部 Switch,显示依赖关系,const 修饰表示只读
三、Bandwidth.h 分析
class Bandwidth {
public:
// 显式构造,初始化值对象
explicit Bandwidth(unsigned int value);
// 值对象比较方法
bool operator==(const Bandwidth& other) const;
private:
const unsigned int value; // 值对象核心属性
const unsigned int expandFactor; // 值对象附加属性,不可修改
};
理解
- 不可变类:
value和expandFactor都是const - 值比较:
operator==,用于判断值对象是否相等 - 表达值语义:没有独立标识,完全由属性值决定
- 扩展性:可以增加其他只读属性,但不改变对象状态
四、属性与依赖映射规则
| C++ 成员类型 | UML 表达 | 含义 | 生命周期语义 |
|---|---|---|---|
| 值类型(如 Bandwidth) | 聚合 o-- |
属性/值对象 | 对象完全拥有,不依赖外部 |
| 引用类型(如 const Switch&) | 聚合/依赖 | 显示依赖 | 生命周期依赖外部,但只读 |
| 指针类型 | 聚合/依赖 | 显示依赖,可修改 | 依赖外部对象,可能为空 |
| const 成员 | N/A | 只读 | 不可修改,保持不可变性 |
五、UML 类图示例
- 空心菱形
o--→ 值对象聚合 方块所在的地方是整体 箭头指向的是被动的对象,比如被依赖的对象,被继承的对象,被包含的对象。 - 箭头
o-->→ 显示依赖 Switch ->箭头指向的是被依赖的对象 - const 引用 → 只读访问,生命周期独立
六、设计原则总结
- 区分属性与依赖
- 属性 → 值对象或基本类型
- 依赖 → 指针或引用
- 实体标识
- 用 id 表达唯一性 →
PortId id - 便于比较和哈希
- 用 id 表达唯一性 →
- 值对象不可变
- 所有属性
const - 提供比较方法
operator==
- 所有属性
- 显示依赖
- 引用或指针类型
- const 修饰只读
- 生命周期外部管理
✓ 总结:
- Port 的属性分为 实体标识 (id)、值对象 (bandwidth)、依赖 (host)
- 值对象不可变,提供比较方法
- 依赖显式化,const 修饰只读
- UML 聚合/依赖箭头对应 C++ 类型和生命周期语义
C++ 实现领域模型时“表达关系”
一、表达关系的核心概念
在领域建模中,类与类之间的关系主要有以下几类:
| 关系类型 | UML 符号 | 含义 | C++ 映射 | 生命周期/依赖特点 | |
|---|---|---|---|---|---|
| is-a | `< | –` | 子类化/接口继承 | class Child : public Parent |
子类对象包含父类部分,继承父类行为 |
| has-a (组合/聚合) | *--/o-- |
整体-部分关系 | 成员对象或指针/引用 | 组合:部分生命周期依赖整体;聚合:部分可独立 | |
| has-many | 集合 + 聚合/组合 | 多个部分对象 | std::vector<Part> 或 std::list<Part*> |
生命周期可依赖或独立,通常通过 Repository 管理 | |
| dependency | ..> |
临时使用/方法参数/调用 | 方法参数引用或指针 | 不影响对象存在性,生命周期独立 |
二、C++ 表达关系示例
1⃣ is-a(继承)
class EventHandler {
public:
virtual Status handle(const Event& e) const = 0;
virtual ~EventHandler() = default;
};
// ConnectionSetupService 是 EventHandler 的实现
class ConnectionSetupService : public EventHandler {
public:
Status handle(const Event& e) const override;
};
- 概念:ConnectionSetupService 是 EventHandler → is-a
- 生命周期:子类对象包含父类部分,继承行为
- 原则:接口继承优先于实现继承,组合优先于实现继承
2⃣ has-a(聚合/组合)
class Switch {
std::vector<Port> ports; // 组合,Port 生命周期依赖 Switch
};
class Port {
const Switch& host; // 聚合,生命周期独立,显示依赖
PortId id;
Bandwidth bandwidth;
};
- 组合
*--:Switch 拥有多个 Port(Port 随 Switch 生命周期销毁) - 聚合
o--:Port 对 Switch 有依赖,但生命周期独立 - C++ 映射:
- 组合 → 成员对象或智能指针
- 聚合 → const 引用或指针
3⃣ has-many(多对多关系)
#include <vector>
class Repository {
std::vector<Port*> ports; // 聚合,多端口,Repository 管理
public:
void addPort(Port* p);
Port* findPortById(PortId id) const;
};
- 概念:对象拥有多个子对象
- 容器选择:
- 生命周期依赖整体 → 使用值对象或智能指针
- 生命周期独立 → 使用裸指针或引用
- 依赖关系反转:
- 客户端依赖抽象 Repository 接口而不是具体实现
4⃣ dependency(依赖)
class ConnectionSetupService {
public:
void setupConnection(const PeerNode& peer); // 临时依赖 PeerNode
};
- 特点:
- 不影响对象存在性
- 生命周期独立
- 保持物理依赖与逻辑依赖一致(方法参数、返回值)
三、UML 类图示例
为了方便记忆把箭头指向的认为是被动的对象,没箭头和方块默认是带箭头的
比如箭头指向EventHandler EventHandler 就是被继承,箭头指向PeerNode,PeerNode 就是被依赖,Switch实体方块然后指向Port Port 就是被Switch 拥有,Port 空菱形+执行Switch Switch 被Port 引用。 被指向的对象是被动对象
理解
- is-a:接口继承,ConnectionSetupService 是 EventHandler
- has-a:
- 组合:Switch 拥有 Port,Port 随 Switch 生命周期销毁
- 聚合:Port 引用 Switch,生命周期独立
- has-many:Repository 管理多个 Port
- dependency:方法参数 PeerNode,生命周期独立
四、C++ 表达关系设计原则
- is-a
- 接口继承优先,减少实现继承
- 子类实现父类行为,private 方法可实现接口隔离
- has-a
- 显式化生命周期关系
- 组合/聚合根据依赖关系选择成员类型(值对象/指针/引用)
- has-many
- 选择合适容器
- 使用 Repository 管理多实例
- 遵循依赖反转原则 → 面向接口编程
- dependency
- 临时使用,不影响对象存在性
- 方法参数引用/指针
- 保持逻辑依赖和物理依赖一致
✓ 总结口诀
is-a 子类化,has-a 拥有关系,has-many 容器管理,dependency 临时用,组合生命周期依赖,聚合可独立存在。
C++ 领域模型“表达关系:is-a”
一、UML 类图解析(is-a)
理解
- is-a(继承 / 泛化):子类是父类的具体类型
- 实例化优先表达数据变化:
- 通过创建不同类型对象(PhysicalSwitch / VirtualSwitch)实现不同数据特性
- 行为变化通过多态表达:
- 虚函数
getBandExpandFactor(),子类覆写父类行为
- 虚函数
二、C++ 代码解析
1⃣ Switch.h(父类)
class Switch {
public:
// 构造函数:初始化端口数量
Switch(unsigned int portNum)
: portNum(portNum) {}
virtual ~Switch() {} // 虚析构函数保证多态销毁
// 对外行为接口
Status expandBand();
private:
// 子类必须实现的虚函数,行为变化通过多态表达
virtual unsigned int getBandExpandFactor() const = 0;
private:
unsigned int portNum; // 数据属性
};
理解
- 数据变化
portNum是 Switch 的实例属性,实例化不同对象表示不同端口数量
- 行为变化
getBandExpandFactor()是纯虚函数- 子类覆写实现不同行为 → 多态
2⃣ VirtualSwitch.h(子类)
class VirtualSwitch : public Switch {
public:
VirtualSwitch(unsigned int portNum)
: Switch(portNum) {}
private:
// 行为变化:虚函数覆写
unsigned int getBandExpandFactor() const override {
return 128; // 虚拟交换机带宽扩展因子
}
};
- is-a:VirtualSwitch 是 Switch → 可以通过 Switch* 或 Switch& 调用 polymorphic 行为
- 数据变化:通过实例化传递不同
portNum - 行为变化:覆写
getBandExpandFactor()
3⃣ PhysicalSwitch.h(子类)
class PhysicalSwitch : public Switch {
public:
PhysicalSwitch(unsigned int portNum)
: Switch(portNum) {}
private:
// 行为变化:虚函数覆写
unsigned int getBandExpandFactor() const override {
return 64; // 物理交换机带宽扩展因子
}
};
- is-a:PhysicalSwitch 是 Switch
- 行为变化:不同子类实现不同带宽扩展逻辑
- 数据变化:通过实例化不同对象控制端口数量
三、设计原则总结
| 设计角度 | 实现方式 | 理解 | |
|---|---|---|---|
| 数据变化 | 实例化不同子类对象 | 不同对象代表不同数据状态 | |
| 行为变化 | 虚函数多态 | 子类覆写方法,实现不同行为 | |
| is-a 关系 | 继承(< | –) | 子类是父类的特化,实现父类接口 |
| 多态访问 | 父类指针或引用 | 对外统一接口,内部行为可变化 |
四、示例调用
Switch* sw1 = new PhysicalSwitch(4);
Switch* sw2 = new VirtualSwitch(8);
// 数据变化通过实例不同对象
std::cout << sw1->getBandExpandFactor(); // 输出 64
std::cout << sw2->getBandExpandFactor(); // 输出 128
delete sw1;
delete sw2;
- 数据变化 → 构造不同实例(端口数不同)
- 行为变化 → 多态调用 getBandExpandFactor()
五、总结
- is-a 表达关系
- 子类继承父类 → 泛化/接口实现
- UML
<|-- - 多态覆盖行为变化
- 数据变化优先实例化
- 构造不同对象表示不同状态
- 避免在父类中硬编码数据
- 行为变化使用多态
- 父类定义虚函数
- 子类覆写不同实现
- 外部统一接口调用,无需关心具体类型
✓ 结论:
数据变化靠实例化(不同对象),行为变化靠多态(虚函数覆写),Switch / PhysicalSwitch / VirtualSwitch 完整表达 is-a 关系,符合领域驱动设计原则。
C++ 领域模型“表达关系:继承”
一、UML 类图解析
理解
- AlarmDetector
- 定义报警检测接口(纯虚类)
- is-a 关系被 Switch 和 Port 实现
- 对外暴露
hasAlarm()方法
- Switch / Port
- 继承 AlarmDetector → 公有继承(is-a)
- 表示 Switch/Port 具有报警检测能力
- 外部可通过 AlarmDetector 接口访问多态行为
- Port 组合 Bandwidth
- 实心菱形
*-->表示组合 - Port 拥有 Bandwidth,生命周期绑定
- 实心菱形
- AlarmReporter 聚合 AlarmDetector
- 空心菱形
o-->表示依赖 - AlarmReporter 可以使用多个 AlarmDetector 对象
- 生命周期独立
- 空心菱形
二、C++ 代码解析
1⃣ AlarmDetector.h(接口)
DEF_INTERFACE(AlarmDetector)
{
ABSTRACT(bool hasAlarm() const);
};
- 宏
DEF_INTERFACE和ABSTRACT封装纯虚类 - 方法
hasAlarm()用const修饰,表示只读 - 接口隔离原则:客户只依赖接口,不依赖具体实现
2⃣ Port.h(实现类)
#include "AlarmDetector.h"
#include "Bandwidth.h"
class Port : public AlarmDetector, private Bandwidth {
public:
// 构造函数:初始化 PortId
Port(PortId id);
Connection* connect(const PeerNode& peer);
private:
// 覆写接口方法,private化,禁止从具体类直接调用
bool hasAlarm() const override;
private:
PortId id;
// 继承 Bandwidth 表示组合关系(生命周期绑定)
};
理解
- 公有继承 AlarmDetector
Port is-a AlarmDetector- 客户可以通过 AlarmDetector* / AlarmDetector& 调用
hasAlarm() - 覆写方法 private 化 → 外部不能直接调用具体类实现
- 私有继承 Bandwidth
- 实现组合(组合关系 1:1)
- 生命周期绑定 → Port 销毁时 Bandwidth 也销毁
- 本质上是一种语法上的“组合”,而非接口继承
三、公有继承 vs 私有继承
| 继承类型 | C++ 语法 | 语义 | 生命周期 | 使用场景 |
|---|---|---|---|---|
| 公有继承 | class Child : public Parent |
is-a,子类是父类 | 子类包含父类 | 接口/角色继承,允许多态访问 |
| 私有继承 | class Child : private Parent |
实现细节,类似组合 | 子类控制父类生命周期 | 组合实现,行为复用,不暴露接口 |
| 保护继承 | class Child : protected Parent |
类内部使用继承接口 | 生命周期绑定 | 子类内部复用,外部不可访问父类接口 |
四、UML 与 C++ 映射总结
| UML 符号 | C++ 映射 | 理解 | |
|---|---|---|---|
| `< | –` | public 继承 |
is-a 关系,接口继承 |
*--> |
private 继承或成员对象 |
强生命周期绑定,组合关系 | |
o--> |
成员引用/指针 | 聚合,生命周期独立 |
五、设计原则总结
- 公有继承
- 接口或角色继承
- 客户通过父类接口访问多态行为
- 覆写方法可 private → 避免直接调用具体类
- 私有继承
- 实现组合语义,生命周期绑定
- 不对外暴露接口,行为复用
- 等价于组合的一种实现方式
- 组合 vs 聚合
- 组合:实心菱形,子对象随整体销毁 →
private Bandwidth或成员对象 - 聚合:空心菱形,子对象独立 → 成员引用或指针
- 组合:实心菱形,子对象随整体销毁 →
- 多态与行为变化
- 接口继承用于行为抽象
- 子类覆写虚函数 → 实现不同行为
六、示例调用
Port p(PortId(1));
AlarmDetector* detector = &p;
// 调用多态接口
bool alarm = detector->hasAlarm();
// 外部无法直接访问 Bandwidth,因为是私有继承
// p.getBandwidth(); // 错误
- 多态访问 → 公有继承 AlarmDetector
- 组合实现 → 私有继承 Bandwidth,不对外暴露
- 生命周期绑定 → Port 销毁时 Bandwidth 自动销毁
✓ 总结口诀
公有继承 is-a 接口/角色,私有继承实现组合/行为复用,组合生命周期随整体,聚合依赖独立存在,多态行为通过接口访问。
表达关系:has-a
C++ 领域模型组合与聚合实现模式
一、组合(Composition)与聚合(Aggregation)概念
| 关系类型 | UML 符号 | C++ 映射 | 生命周期语义 | 设计原则 |
|---|---|---|---|---|
| 组合(Composition) | 实心菱形 *-- |
成员对象 或 私有继承 private |
子对象生命周期与宿主一致 | 强烈绑定,宿主销毁时子对象自动销毁 |
| 聚合(Aggregation) | 空心菱形 o-- |
引用 & 或 指针 * |
子对象可独立存在 | 宿主与部分对象生命周期不同,依赖注入明确 |
二、组合优先推荐的实现方式
- 值包含(成员对象)
- 子对象作为宿主成员对象声明
- 生命周期与宿主一致
- 不对外暴露成员内部实现
- 私有继承(private inheritance)
- 相当于组合的一种语法实现
- 子对象功能复用,但不暴露接口
- 生命周期随宿主类
示例:Port 私有继承 Bandwidth(组合)
class Port : private Bandwidth { // Composition: Port 拥有 Bandwidth,生命周期绑定
public:
Port(PortId id, const Switch& s);
Status connect(const Connection& conn); // 对外行为
void release(const Connection& conn);
private:
PortId id; // 唯一标识
Connection* conn; // 聚合或依赖对象
const Switch& host; // 引用聚合,生命周期早于宿主
};
三、聚合实现原则
- 使用引用(&)
- 当聚合对象 生命周期早于宿主
- 通过构造函数注入(Constructor Dependency Injection)
- 使用
const保证只读访问
- 使用指针(*)
- 当聚合对象 生命周期晚于宿主
- 可以通过 setter 或接口更新(update)
四、Port.h 分析
class Port : private Bandwidth {
public:
// 构造函数:Port 依赖 Switch,生命周期早于宿主
Port(PortId id, const Switch& s);
// 对外接口:建立连接
Status connect(const Connection& conn);
// 对外接口:释放连接
void release(const Connection& conn);
private:
PortId id; // 实体标识
Connection* conn; // 聚合/依赖对象,生命周期晚于宿主
const Switch& host; // 引用聚合,生命周期早于宿主
};
理解
- 组合:
private Bandwidth→ Port 拥有 Bandwidth- 生命周期绑定
- 外部不能直接访问 Bandwidth
- 聚合 / 依赖:
const Switch& host→ 生命周期早于宿主,通过构造函数注入Connection* conn→ 生命周期晚于宿主,可通过方法更新
- 行为接口:
connect()与release()提供对外操作- 内部状态通过成员对象和指针维护
五、UML 类图示例
理解
- 实心菱形
*--→ 组合,Port 拥有 Bandwidth - 空心菱形
o--→ 聚合,Port 引用 Switch - 依赖箭头
..>→ 临时/晚于宿主的对象,Port 依赖 Connection
六、设计原则总结
- 组合(Composition)
- 强生命周期绑定,宿主销毁 → 子对象自动销毁
- 优先使用值对象成员或私有继承
- 聚合(Aggregation)
- 生命周期可独立
- 生命周期早于宿主 → 使用引用 + 构造函数注入
- 生命周期晚于宿主 → 使用指针 + update 接口
- 接口设计
- 对外接口只暴露必要行为(connect/release)
- 内部成员保持封装和生命周期管理
✓ 口诀总结
组合强绑定,值包含或私有继承;聚合生命周期早用引用,晚用指针;行为接口最小化暴露,const 表示只读。
C++ 领域模型“表达关系:has-many”
一、has-many 的概念
- has-many:一个对象拥有多个其他对象的关系
- 依赖方向设计:
- host -> items
- 宿主持有多个子对象
- 通常用指针或容器管理(
std::vector<Item*>或数组) - 宿主管理子对象生命周期
- item -> host
- 子对象持有宿主的引用
- 生命周期早于宿主
- 构造函数注入,保证只读访问
- host -> items
- 双向引用:
- 尽量避免直接双向引用,避免循环依赖
- 可让 items 持有宿主接口的一个角色,减少耦合
二、C++ 代码分析(Port 与 Switch)
class Port : private Bandwidth {
public:
Port(PortId id, const Switch& s); // 构造函数注入宿主
Status connect(const Connection& conn);
void release(const Connection& conn);
private:
PortId id; // 唯一标识
Connection* conn[MAX_CONN_NUM]; // 容器管理多个 Connection → has-many
const Switch& host; // 宿主引用 → 生命周期早于 Port
};
理解
- host -> items
Switch拥有多个Port→ 聚合关系- 生命周期可以独立或绑定
- UML 用空心菱形
o-- "1..N"表示
- Port -> host
host引用宿主Switch- 生命周期早于
Port,构造函数注入 - 保证只读访问(
const Switch&)
- Port -> Connection
- Port 维护多个 Connection → has-many
- 通过指针数组
Connection* conn[MAX_CONN_NUM] - 聚合关系,Connection 生命周期可独立
三、UML 类图示例
理解
- 实心菱形
*--→ 组合- Port 拥有 Bandwidth,生命周期绑定
- 空心菱形
o--→ 聚合- Switch 拥有多个 Port,Port 生命周期可以独立
- Port 拥有多个 Connection,Connection 生命周期可以独立
- 箭头方向
- 宿主指向 items,表示管理多个对象
- items 可以持有宿主引用,保证依赖和生命周期早于 items
四、设计原则总结
- has-many 宿主持有 items
- 使用指针或容器
- 宿主负责生命周期或管理
- 容器大小可固定(数组)或动态(vector)
- items 持有宿主引用
- 生命周期早于 items → 构造函数注入
- 使用
const保证只读
- 双向引用尽量避免
- 如果必须,items 持有宿主接口而非完整类型
- 减少循环依赖
- 组合 vs 聚合
- 组合(Port -> Bandwidth) → 生命周期绑定
- 聚合(Switch -> Port, Port -> Connection) → 生命周期可独立
五、示例调用
Switch sw;
Port p1(PortId(1), sw);
Port p2(PortId(2), sw);
// Port 连接多个 Connection
Connection c1, c2;
p1.connect(c1);
p1.connect(c2);
- Switch -> Port → 聚合
- Port -> Connection → 聚合
- Port -> Bandwidth → 组合,内部管理
✓ 口诀总结
宿主持有多个 items 用指针或容器(has-many);items 持有宿主引用,生命周期早于 items;组合实心菱形生命周期绑定,聚合空心菱形生命周期可独立。
dependency(依赖)
一、UML 图解析
理解
1⃣ Port …> PeerNode(dependency)
- 虚线箭头
..>表示依赖 - 语义:Port 使用 PeerNode
- 不是生命周期关系
- 不是拥有关系
- 只是“功能使用”
dependency 是“功能需要”,不是“存在需要”。
2⃣ Port o-- Connection(聚合)
- 空心菱形表示聚合
- 表示 Port 持有多个 Connection
- 生命周期可以独立
二、dependency 的本质
dependency 是“功能上的需要”,不是“存在上的需要”。
可以形式化理解:
如果对象 A 的存在不依赖 B,则:
Exist(A)⇏Exist(B) Exist(A) \not\Rightarrow Exist(B) Exist(A)⇒Exist(B)
但如果 A 调用 B 的功能,则:
Use(A,B) Use(A,B) Use(A,B)
这就是 dependency。
三、C++ 中 dependency 的三种表达方式
1⃣ 参数依赖(use 关系)
最常见的 dependency 表达方式。
class PeerNode; // 前置声明,减少物理依赖
class Port {
public:
// 参数依赖
Status connect(const PeerNode& peer) const;
};
说明
const PeerNode&- 表示:
- 功能使用
- 不修改对象
- 不拥有对象
- 生命周期独立
原则:
- 尽量使用 引用
- 加上
const - 不使用值传递(避免复制)
- 不使用指针(除非允许为空)
2⃣ 返回值依赖(create 关系)
用于表达“创建关系”。
创建实体 → 返回指针
class Connection;
class Port {
public:
Connection* createConnection(const PeerNode& peer);
};
为什么返回指针?
- 实体有身份
- 生命周期可独立
- 可能由调用者管理
创建值对象 → 返回值类型
class Bandwidth {
public:
explicit Bandwidth(unsigned int v);
};
Bandwidth Port::calculateBandwidth() const;
值对象:
- 无身份
- 可复制
- 生命周期简单
四、减少 dependency 的物理依赖
1⃣ 使用前置声明
✗ 错误做法:
#include "PeerNode.h"
✓ 正确做法:
class PeerNode; // 前置声明
前置声明即可满足:
- 指针
- 引用
- 函数参数
- 返回指针
2⃣ 何时必须 include?
只有在以下情况必须 include:
- 需要对象大小(成员变量)
- 需要内联实现
- 需要访问具体成员
否则:
头文件只声明,cpp文件包含
五、dependency vs aggregation 对比
| 特性 | dependency | aggregation |
|---|---|---|
| UML | ..> |
o-- |
| 是否拥有 | ✗ 不拥有 | ✗ 不一定拥有 |
| 生命周期依赖 | ✗ 无 | ✗ 可能 |
| 表达方式 | 参数/返回值 | 成员变量 |
| 是否存储 | ✗ 不存储 | ✓ 存储 |
六、完整示例(整合)
// 前置声明
class PeerNode;
class Connection;
class Port {
public:
// dependency:use 关系
Status connect(const PeerNode& peer);
// create 关系
Connection* createConnection(const PeerNode& peer);
private:
// 聚合:has-many
Connection* connections[MAX_CONN_NUM];
};
七、设计原则总结
dependency 设计准则
- 只用于表达“功能使用”
- 尽量使用 const 引用
- 返回实体用指针
- 返回值对象用值
- 头文件用前置声明
- 不在头文件 include 依赖类
八、dependency 的一句话口诀
dependency 是功能需要,不是存在需要;参数用引用,返回看语义;减少物理依赖,头文件只声明。
九、与其他关系对比总结
| 关系 | 关键词 | 生命周期 | 是否拥有 |
|---|---|---|---|
| 继承 | is-a | 绑定 | 是 |
| 组合 | has-a | 强绑定 | 是 |
| 聚合 | has-a | 可独立 | 否 |
| has-many | 多个 | 可独立 | 否 |
| dependency | use | 无关 | 否 |
领域模型在 C++ 中的核心难点 —— 生命周期设计(Lifecycle Design)。
领域模型是否健壮,本质取决于:
对象的生命周期=领域语义+所有权模型 \text{对象的生命周期} = \text{领域语义} + \text{所有权模型} 对象的生命周期=领域语义+所有权模型
如果生命周期表达错,领域语义就会被破坏。
一、生命周期管理的本质
1⃣ 生命周期是什么?
对象从:
构造→使用→销毁 \text{构造} \rightarrow \text{使用} \rightarrow \text{销毁} 构造→使用→销毁
完整过程就是生命周期。
形式化表达:
Lifetime(O)=[tcreate,tdestroy) Lifetime(O) = [t_{create}, t_{destroy}) Lifetime(O)=[tcreate,tdestroy)
2⃣ 所有权是什么?
所有权是:
谁负责销毁对象?
如果对象 A 负责销毁对象 B:
Owner(A,B)=true Owner(A,B) = true Owner(A,B)=true
二、领域模型中的生命周期设计原则
原则一:生命周期必须表达领域语义
例如:
- Switch 拥有 Port
- Port 不拥有 PeerNode(只是 use)
如果代码表达成:
PeerNode peer; // 组合
那语义就错了。
原则二:所有权必须唯一(避免双重释放)
∑Owner(Oi,X)=1 \sum Owner(O_i, X) = 1 ∑Owner(Oi,X)=1
一个对象只能有一个所有者。
三、Factory / Repository 模式
在领域模型中:
对象的创建与销毁不应该由业务类直接负责
而是交给:
- Factory(创建)
- Repository(生命周期 + 存储管理)
1⃣ Factory 示例
class Connection {
public:
Connection(int id) : id_(id) {}
private:
int id_;
};
class ConnectionFactory {
public:
// 负责创建对象
static Connection* create(int id) {
return new Connection(id);
}
};
解释
- 隐藏构造细节
- 隐藏内存分配
- 业务类不直接 new
2⃣ Repository 示例
#include <vector>
#include <memory>
class ConnectionRepository {
public:
Connection& add(int id) {
storage_.push_back(std::make_unique<Connection>(id));
return *storage_.back();
}
private:
std::vector<std::unique_ptr<Connection>> storage_;
};
解释
- Repository 持有对象
- 统一销毁
- 业务类不关心 delete
四、嵌入式环境的特殊考虑
嵌入式系统通常:
- 无堆
- 或堆受限
- 或必须可预测
1⃣ 重载 new / delete
用于:
- 定制内存区域
- 使用专用内存池
class MyObject {
public:
static void* operator new(std::size_t size) {
return my_memory_alloc(size);
}
static void operator delete(void* ptr) {
my_memory_free(ptr);
}
};
语义:
控制:
Memory=f(domainpolicy) Memory = f(domain_policy) Memory=f(domainpolicy)
2⃣ placement new(指定内存构造)
用于:
- 预分配内存
- 延迟构造
#include <new>
char buffer[sizeof(Connection)];
Connection* conn = new (buffer) Connection(42);
解释
- 不分配内存
- 只调用构造函数
- 生命周期分离为:
Memory_Lifetime≠Object_Lifetime Memory\_Lifetime \neq Object\_Lifetime Memory_Lifetime=Object_Lifetime
3⃣ Object Pool(对象池)
思想:
- 静态确定最大数量
- 预分配
- 重复使用
class ConnectionPool {
public:
Connection* allocate(int id) {
for (auto& slot : pool_) {
if (!slot.in_use) {
slot.in_use = true;
return new (&slot.storage) Connection(id);
}
}
return nullptr;
}
private:
struct Slot {
bool in_use = false;
alignas(Connection) char storage[sizeof(Connection)];
};
Slot pool_[10];
};
数学理解
最大容量:
N=10 N = 10 N=10
时间复杂度:
Talloc=O(N) T_{alloc} = O(N) Talloc=O(N)
但:
- 无堆分配
- 可预测
- 无碎片
五、时序图解析
详细理解
1⃣ User 调用 ObjectA
表示:
- User 不拥有 ObjectA
- 只是使用
这是 dependency。
2⃣ ObjectA 自调用
说明:
- 内部状态机
- 内部处理流程
3⃣ ObjectA 调用 ObjectB
两种可能:
- dependency(仅 use)
- aggregation(成员对象)
如果是 dependency:
void ObjectA::process(ObjectB& b);
如果是 aggregation:
class ObjectA {
ObjectB b_; // 生命周期绑定
};
激活条的含义
激活条表示:
Active(O,t)=true Active(O, t) = true Active(O,t)=true
对象处于执行状态。
六、生命周期的三种模型
| 模型 | 特点 | C++ 表达 |
|---|---|---|
| 强绑定 | 同生共死 | 成员对象 |
| 弱绑定 | 可独立 | 指针 / unique_ptr |
| 无绑定 | 仅使用 | 参数引用 |
七、领域生命周期口诀
语义先于代码,所有权必须唯一;
创建交给工厂,存储交给仓库;
嵌入式不用堆,对象池来管理;
内存和对象分离,placement 要牢记。
八、核心思想总结
生命周期设计的核心公式:
DomainCorrectness=OwnershipCorrectness Domain_Correctness = Ownership_Correctness DomainCorrectness=OwnershipCorrectness
如果所有权模型错误:
- 会双删
- 会泄漏
- 会悬空
- 会破坏领域一致性
领域模型实现的核心哲学生命周期:
生命周期是“时间维度”,所有权是“责任维度”。
两者必须同时正确,领域模型才是安全的。
一、生命周期设计(Lifecycle Design)
1⃣ 拆分不同生命周期的领域对象
在领域中,不同对象往往对应不同业务时间段。
形式化表示:
Lifetime(Oi)=[tstart,tend) Lifetime(O_i) = [t_{start}, t_{end}) Lifetime(Oi)=[tstart,tend)
如果两个对象业务时间不同:
Lifetime(A)≠Lifetime(B) Lifetime(A) \ne Lifetime(B) Lifetime(A)=Lifetime(B)
那它们就不应该使用组合(强绑定)。
示例
- 订单(Order)生命周期:下单 → 完成
- 订单项(OrderItem)生命周期:创建 → 订单销毁
显然:
Lifetime(OrderItem)⊂Lifetime(Order) Lifetime(OrderItem) \subset Lifetime(Order) Lifetime(OrderItem)⊂Lifetime(Order)
因此: - Order 组合 OrderItem(成员对象或 unique_ptr)
2⃣ 生命周期必须与业务时间一致
原则:
领域对象的构造和销毁必须对应真实业务事件。
例如:
- 用户注册 → 创建 User
- 用户注销 → 销毁 User
代码中必须明确表达:
class UserFactory {
public:
static std::unique_ptr<User> create(UserId id) {
return std::make_unique<User>(id);
}
};
3⃣ 使用 Factory / Repository 管理生命周期
Factory 负责“开始”
class OrderFactory {
public:
static std::unique_ptr<Order> create(OrderId id) {
return std::make_unique<Order>(id);
}
};
Repository 负责“存续 + 结束”
#include <vector>
#include <memory>
class OrderRepository {
public:
Order& add(OrderId id) {
orders_.push_back(OrderFactory::create(id));
return *orders_.back();
}
void remove(OrderId id) {
// 删除对应订单
}
private:
std::vector<std::unique_ptr<Order>> orders_;
};
说明:
- Repository 持有 unique_ptr
- 生命周期清晰可控
- 销毁发生在容器删除时
二、所有权设计(Ownership Design)
1⃣ 所有权 ≠ 生命周期
重要区分:
- 生命周期:对象活多久
- 所有权:谁负责销毁
形式化:
Owner(A,B)=true⇒A负责B的销毁 Owner(A, B) = true \Rightarrow A 负责 B 的销毁 Owner(A,B)=true⇒A负责B的销毁
但:
Lifetime(A)≠Lifetime(B) Lifetime(A) \ne Lifetime(B) Lifetime(A)=Lifetime(B)
未必相等。
三、unique_ptr:唯一所有权
1⃣ 表达语义
- 唯一所有者
- 生命周期绑定
- 可转移
- 不可共享
数学表达:
∑Owner(Oi,X)=1 \sum Owner(O_i, X) = 1 ∑Owner(Oi,X)=1
2⃣ 示例
#include <memory>
class Node {
public:
Node(int v) : value(v) {}
private:
int value;
};
class Tree {
public:
void setRoot(std::unique_ptr<Node> root) {
root_ = std::move(root); // 转移所有权
}
private:
std::unique_ptr<Node> root_; // 唯一所有者
};
说明:
- Tree 是权威持有者
- root 生命周期由 Tree 管理
四、shared_ptr:共享所有权
1⃣ 特点
- 引用计数
- 多个 owner
- 生命周期隐式结束
生命周期结束条件:
RefCount(X)=0 RefCount(X) = 0 RefCount(X)=0
2⃣ 示例
#include <memory>
class Session {
};
std::shared_ptr<Session> createSession() {
return std::make_shared<Session>();
}
风险:
- 生命周期不清晰
- 容易循环引用
3⃣ 循环引用问题
class A {
public:
std::shared_ptr<B> b;
};
class B {
public:
std::shared_ptr<A> a; // 循环引用
};
结果:
RefCount(A)≥1∧RefCount(B)≥1 RefCount(A) \ge 1 \quad \land \quad RefCount(B) \ge 1 RefCount(A)≥1∧RefCount(B)≥1
永远不会释放。
五、weak_ptr:弱引用
1⃣ 语义
- 不拥有
- 不增加引用计数
- 仅访问句柄
2⃣ 示例
#include <memory>
class A;
class B {
public:
std::weak_ptr<A> parent; // 不拥有
};
使用前必须检查:
if (auto p = parent.lock()) {
// 安全访问
}
六、通过设计简化生命周期
最重要的一条:
通过拓扑顺序保证生命周期正确
示例:树结构
生成顺序:
Root→Child→Leaf Root \rightarrow Child \rightarrow Leaf Root→Child→Leaf
销毁顺序(自动):
Leaf→Child→Root Leaf \rightarrow Child \rightarrow Root Leaf→Child→Root
正确设计
#include <memory>
#include <vector>
class Node {
public:
explicit Node(Node* parent = nullptr)
: parent_(parent) {}
void addChild(std::unique_ptr<Node> child) {
children_.push_back(std::move(child));
}
private:
Node* parent_; // 裸指针或引用,不拥有
std::vector<std::unique_ptr<Node>> children_; // 拥有子节点
};
解释
- 父节点拥有子节点
- 子节点只引用父节点
- 生命周期单向流动
- 无循环 ownership
满足:
Owner(Parent,Child)=true Owner(Parent, Child) = true Owner(Parent,Child)=true
Owner(Child,Parent)=false Owner(Child, Parent) = false Owner(Child,Parent)=false
七、所有权设计黄金法则
1⃣ 尽量不共享所有权
共享所有权意味着:
- 生命周期不可预测
- 责任模糊
2⃣ 明确权威持有者
每个对象必须有一个“主所有者”。
3⃣ 只共享只读访问
如果只是访问:
void process(const Order& order);
不需要 shared_ptr。
4⃣ 优先使用 unique_ptr
原则:
能 unique 就不要 shared
能引用就不要指针
能值对象就不要堆对象
八、生命周期 + 所有权统一模型
可以总结为:
Domain_Safety=Correct(Lifetime)+Correct(Ownership) Domain\_Safety = Correct(Lifetime) + Correct(Ownership) Domain_Safety=Correct(Lifetime)+Correct(Ownership)
九、总结口诀
生命周期看业务时间,
所有权看销毁责任;
唯一持有用 unique,
共享持有要谨慎;
弱引用只做访问,
设计优于智能指针。
placement new 在领域模型与嵌入式系统中的使用模式
我们分层讲清楚:
一、什么是 placement new?
1⃣ 标准 new 做了两件事
new T(args);
等价于:
$$
- 分配内存 \
- 调用构造函数
$$
2⃣ placement new 只做一件事
new (memory) T(args);
它只做:
Construct(T,memory) Construct(T, memory) Construct(T,memory)
不分配内存。
二、placement 的核心作用
1⃣ 在指定内存上构造对象
#include <new>
char buffer[sizeof(int)];
int* p = new (buffer) int(42); // 在 buffer 上构造 int
解释:
- buffer 是预分配内存
- 构造发生在 buffer 上
- 不使用堆
2⃣ 按系统字节对齐要求预占内存
对象必须满足:
Address≡0(modalignof(T)) Address \equiv 0 \pmod{alignof(T)} Address≡0(modalignof(T))
否则行为未定义。
正确做法
#include <new>
#include <cstddef>
alignas(double) char buffer[sizeof(double)];
double* p = new (buffer) double(3.14);
或者:
std::aligned_storage_t<sizeof(T), alignof(T)> storage;
三、为必须有参构造对象预占内存
有些对象:
- 没有默认构造
- 必须带参数
- 又不能使用堆
这时:
struct Device {
Device(int id);
};
alignas(Device) char buffer[sizeof(Device)];
Device* d = new (buffer) Device(10);
四、为数组预占内存
alignas(Device) char buffer[10 * sizeof(Device)];
Device* arr = reinterpret_cast<Device*>(buffer);
for (int i = 0; i < 10; ++i) {
new (&arr[i]) Device(i);
}
销毁必须手动调用:
for (int i = 0; i < 10; ++i) {
arr[i].~Device();
}
五、和 union 结合实现静态多态
这部分是关键。
你给的类图:
语义:
- Reporter 是接口
- CachedReporter / PromptReporter 是实现
- AlarmReporter 聚合一个 Reporter
六、传统做法(堆 + 多态)
class AlarmReporter {
public:
AlarmReporter(std::unique_ptr<Reporter> r)
: reporter_(std::move(r)) {}
private:
std::unique_ptr<Reporter> reporter_;
};
问题:
- 使用堆
- 生命周期动态
- 嵌入式不友好
七、嵌入式静态多态方案(placement + union)
1⃣ 定义接口
class Reporter {
public:
virtual ~Reporter() = default;
virtual Status report(const AlarmInfo&) = 0;
};
2⃣ 具体实现
class CachedReporter : public Reporter {
public:
explicit CachedReporter(const Address& addr)
: addr_(addr) {}
Status report(const AlarmInfo&) override {
// 缓存发送
}
private:
Address addr_;
};
class PromptReporter : public Reporter {
public:
explicit PromptReporter(const Address& addr)
: addr_(addr) {}
Status report(const AlarmInfo&) override {
// 立即发送
}
private:
Address addr_;
};
3⃣ 使用 union + placement
核心思想:
- 预留足够大内存
- 在其中构造某一种实现
- 运行时只存在一种
计算最大大小
Size=max(sizeof(CachedReporter),sizeof(PromptReporter)) Size = \max(sizeof(CachedReporter), sizeof(PromptReporter)) Size=max(sizeof(CachedReporter),sizeof(PromptReporter))
实现
#include <new>
#include <type_traits>
class AlarmReporter {
public:
enum class Type {
Cached,
Prompt
};
AlarmReporter(Type t, const Address& addr)
: type_(t)
{
if (t == Type::Cached) {
reporter_ = new (&storage_) CachedReporter(addr);
} else {
reporter_ = new (&storage_) PromptReporter(addr);
}
}
~AlarmReporter() {
reporter_->~Reporter(); // 手动析构
}
Status report(const AlarmInfo& info) {
return reporter_->report(info);
}
private:
Type type_;
using Storage =
std::aligned_storage_t<
sizeof(CachedReporter) > sizeof(PromptReporter)
? sizeof(CachedReporter)
: sizeof(PromptReporter),
alignof(CachedReporter)
>;
Storage storage_;
Reporter* reporter_;
};
八、为什么叫“静态多态”?
因为:
- 内存静态确定
- 最大对象大小编译期确定
- 不依赖堆
- 无运行时分配
但仍然有: - 虚函数分发
可以表示为:
Memory=Fixed Memory = Fixed Memory=Fixed
Type=RuntimeSelect Type = Runtime_Select Type=RuntimeSelect
九、生命周期拆分
placement 下:
MemoryLifetime≠ObjectLifetime Memory_Lifetime \neq Object_Lifetime MemoryLifetime=ObjectLifetime
内存可能长期存在,但对象可能:
Construct→Destruct→Reconstruct Construct \rightarrow Destruct \rightarrow Reconstruct Construct→Destruct→Reconstruct
这在嵌入式非常重要。
十、使用 placement 的注意事项
1⃣ 必须手动析构
object->~T();
2⃣ 必须保证对齐
Address≡0(modalignof(T)) Address \equiv 0 \pmod{alignof(T)} Address≡0(modalignof(T))
3⃣ 不能重复构造未析构对象
否则:
未定义行为。
十一、什么时候用 placement?
适用场景:
- 禁止堆
- 内存必须可预测
- 对象数量固定
- 对象类型有限
- 高实时性要求
十二、总结口诀
placement 不分配,只构造;
对齐必须要满足;
生命周期可分离;
union 预留最大体;
析构必须手动调。
十三、核心思想总结
placement 的本质:
Object=Memory+Constructor Object = Memory + Constructor Object=Memory+Constructor
标准 new:
Object=Allocate+Constructor Object = Allocate + Constructor Object=Allocate+Constructor
placement:
Object=Constructor(Memory) Object = Constructor(Memory) Object=Constructor(Memory)
#include <iostream>
#include <new>
#include <type_traits>
#include <string>
// ======================
// 基础领域类型
// ======================
using Status = bool;
struct Address {
std::string ip;
};
struct AlarmInfo {
std::string message;
};
// ======================
// Reporter 接口
// ======================
class Reporter {
public:
virtual ~Reporter() = default;
virtual Status report(const AlarmInfo& info) = 0;
};
// ======================
// CachedReporter 实现
// ======================
class CachedReporter : public Reporter {
public:
explicit CachedReporter(const Address& addr)
: addr_(addr)
{
std::cout << "CachedReporter constructed\n";
}
~CachedReporter() override {
std::cout << "CachedReporter destroyed\n";
}
Status report(const AlarmInfo& info) override {
std::cout << "[Cached] Send to " << addr_.ip
<< " message: " << info.message << "\n";
return true;
}
private:
Address addr_;
};
// ======================
// PromptReporter 实现
// ======================
class PromptReporter : public Reporter {
public:
explicit PromptReporter(const Address& addr)
: addr_(addr)
{
std::cout << "PromptReporter constructed\n";
}
~PromptReporter() override {
std::cout << "PromptReporter destroyed\n";
}
Status report(const AlarmInfo& info) override {
std::cout << "[Prompt] Send to " << addr_.ip
<< " message: " << info.message << "\n";
return true;
}
private:
Address addr_;
};
// ======================
// AlarmReporter
// 使用 placement + 静态内存
// ======================
class AlarmReporter {
public:
enum class Type {
Cached,
Prompt
};
AlarmReporter(Type t, const Address& addr)
: type_(t)
{
if (t == Type::Cached) {
reporter_ = new (&storage_) CachedReporter(addr);
} else {
reporter_ = new (&storage_) PromptReporter(addr);
}
}
~AlarmReporter() {
// 手动调用析构函数
reporter_->~Reporter();
}
Status report(const AlarmInfo& info) {
return reporter_->report(info);
}
private:
Type type_;
// 计算最大对象大小
static constexpr std::size_t MaxSize =
sizeof(CachedReporter) > sizeof(PromptReporter)
? sizeof(CachedReporter)
: sizeof(PromptReporter);
static constexpr std::size_t MaxAlign =
alignof(CachedReporter) > alignof(PromptReporter)
? alignof(CachedReporter)
: alignof(PromptReporter);
using Storage = std::aligned_storage_t<MaxSize, MaxAlign>;
Storage storage_; // 静态内存
Reporter* reporter_; // 指向 placement 构造对象
};
// ======================
// main 测试
// ======================
int main() {
Address addr{"192.168.1.1"};
AlarmInfo info{"Temperature too high"};
{
std::cout << "---- Cached Mode ----\n";
AlarmReporter reporter(
AlarmReporter::Type::Cached, addr);
reporter.report(info);
} // 作用域结束,手动析构触发
{
std::cout << "\n---- Prompt Mode ----\n";
AlarmReporter reporter(
AlarmReporter::Type::Prompt, addr);
reporter.report(info);
}
return 0;
}
更多推荐


所有评论(0)