$ cat ~ / posts /c++ /oop8 3.2k Words ~ 13 Mins
cover.png
面向对象程序设计08

#面向对象程序设计08

exdoubled Lv5

OOD 七步

第一步:业务描述与边界

第一步只谈业务,不谈技术

不要一上来就写“用 Redis 缓存购物车”“用 MySQL 建表”“用消息队列发通知”,这些都是实现细节

此时要做的是把系统解决的问题用业务语言写下来,并且划清边界

以电商系统为例:

一个电商平台允许用户浏览商品、提交订单、选择支付渠道完成支付

系统根据用户等级自动计算折扣,支付成功后订单状态推进,并通过多个渠道通知用户

这个描述真正重要的是边界:

系统内要做系统外不做
用户等级与折扣计算用户注册、登录、认证
商品信息维护库存管理、补货
订单创建与状态流转退款、售后
多种支付渠道执行支付渠道对接细节
支付成功后多渠道通知物流追踪、促销、优惠券

这个边界决定了后面的类是否应该出现

例如“库存”在完整电商平台里很重要,但在本案例中被明确划出系统外,因此 Inventory 不应该出现在当前设计里。否则需求会不断蔓延,一个教学用订单系统很快变成完整电商平台

同时还要建立统一业务术语表:

术语定义
用户在平台下单的主体,分普通用户和 VIP 用户
商品可被购买的物品,包含名称与单价
订单项一种商品加购买数量,是订单的最小单元
订单一次购买行为的完整记录,有生命周期
折扣率用户等级或规则对应的价格系数
支付用户为订单付款的动作,渠道可选
通知系统在关键节点主动告知用户的消息
订单状态订单所处阶段,单向流转,不可随意逆转

第二步:识别领域对象

第二步通常从“找名词”开始。通读业务描述,可以得到:

  • 用户、普通用户、VIP 用户
  • 商品
  • 订单、订单项、订单状态
  • 支付、信用卡、支付宝、微信
  • 通知、邮件、短信
  • 折扣

但不是所有名词都应该直接变成类。需要继续筛选:

类型对象判断
实体UserProductOrder有身份,有生命周期
值对象OrderItemOrderStatus依附于实体,本身不需要独立身份
行为变化点支付、通知、折扣先看作行为,后续决定是否抽象成策略
排除对象购物车、库存、物流不在当前限界上下文中

这里有重要判断:不要把动作直接变成类

比如“支付”首先是订单结算过程中的行为,不应该机械地定义一个 DoPayment 类,是否需要抽象成 PaymentStrategy,要等到分析变化点时再决定

第三步:用 GRASP 分配职责

识别对象后,要问每个对象三个问题:

  • 它知道什么?
  • 它能做什么?
  • 它不该管什么?

这一步是从“数据类”走向“对象”的关键。只有字段没有行为的类,是披着类外衣的结构体;所有逻辑都放在外部服务中,则会变成上帝类。

在电商案例中,职责可以这样划分:

对象知道什么能做什么不该管什么
User姓名、联系方式、用户等级提供用户信息,或提供与用户等级有关的信息不知道订单,不执行支付
Product商品 ID、名称、单价提供商品基础信息不知道谁买了它,不管库存
OrderItem商品、数量计算本行小计 subtotal()不知道订单状态,不管折扣
Order用户、订单项、状态、支付策略、折扣策略、通知器添加商品、计算总价、结账、发货、完成、取消不知道具体支付渠道和通知渠道实现

Order 是核心协调者,但不应该成为“什么都懂”的对象

可以负责订单生命周期推进,但不应该知道信用卡如何扣款、短信如何发送、VIP 折扣公式如何演化

这些变化点应该被隔离

第四步:提炼抽象与设计模式

第四步要找系统中的“变数”。抽象是为了隔离未来高概率变化的部分

本案例中有三个明显变化点:

  • 支付方式可能增加:信用卡、支付宝、微信之外还可能有新的渠道。
  • 通知渠道可能增加:邮件、短信之外还可能有站内信、App 推送。
  • 折扣规则可能变化:普通会员、VIP、活动价、黑金会员等规则可能不同。

这些变化点适合使用策略模式

策略模式的核心是:调用方只依赖稳定接口,具体算法或渠道通过多态替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PaymentStrategy {
public:
virtual std::string pay(double amount) = 0;
virtual ~PaymentStrategy() = default;
};

class AlipayPayment : public PaymentStrategy {
public:
std::string pay(double amount) override {
return "alipay paid " + std::to_string(amount);
}
};

class WechatPayment : public PaymentStrategy {
public:
std::string pay(double amount) override {
return "wechat paid " + std::to_string(amount);
}
};

通知也可以用同样方式抽象:

1
2
3
4
5
6
7
8
9
10
11
12
class Notifier {
public:
virtual void send(const User& user, const std::string& message) = 0;
virtual ~Notifier() = default;
};

class EmailNotifier : public Notifier {
public:
void send(const User& user, const std::string& message) override {
// send email to user.contact()
}
};

当一个订单支付成功后,需要通知多个渠道。这里可以看作观察者模式的简化形式:Order 在状态变化后通知一组 Notifier,而不关心每个通知器具体如何发送

1
2
3
4
5
void notify_all(const User& user, const std::string& message) {
for (const auto& notifier : notifiers_) {
notifier->send(user, message);
}
}

折扣也适合抽象成策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DiscountStrategy {
public:
virtual double calculate(double raw_total) const = 0;
virtual ~DiscountStrategy() = default;
};

class NoDiscount : public DiscountStrategy {
public:
double calculate(double raw_total) const override {
return raw_total;
}
};

class MemberDiscount : public DiscountStrategy {
public:
explicit MemberDiscount(double rate) : rate_(rate) {}

double calculate(double raw_total) const override {
return raw_total * rate_;
}

private:
double rate_;
};

ProductOrderItem 没有明显多态需求,直接写具体类即可

这里不能为了“使用设计模式”而强行引入 ProductStrategyOrderItemFactory。抽象的依据是变化概率

第五步:绘制类图

类图描述系统静态结构

对于本案例,可以得到下面的关系:

  • Order 组合多个 OrderItem,订单不存在时订单项也没有独立意义。
  • OrderItem 引用或保存 Product 信息。
  • Order 关联一个 User
  • Order 依赖 PaymentStrategyDiscountStrategy 和一组 Notifier
  • AlipayPaymentWechatPaymentCreditCardPayment 实现 PaymentStrategy
  • EmailNotifierSMSNotifier 实现 Notifier

用伪 UML 表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
User <---------------- Order
|
| owns
v
OrderItem ----> Product
|
v
OrderStatus

Order ----> PaymentStrategy <|-- AlipayPayment
Order ----> PaymentStrategy <|-- WechatPayment
Order ----> PaymentStrategy <|-- CreditCardPayment

Order ----> DiscountStrategy <|-- NoDiscount
Order ----> DiscountStrategy <|-- MemberDiscount

Order ----> Notifier <|-- EmailNotifier
Order ----> Notifier <|-- SMSNotifier

类图的作用不是画得漂亮,而是提前暴露复杂度

如果所有类互相依赖,图像像一张网,后续实现和修改都会困难

一个可维护的设计通常应该让核心对象少知道具体实现,多依赖稳定抽象

第六步:用时序图验证交互

静态类图只能说明“有哪些类”和“类之间有什么关系”,不能证明业务流程走得通。第六步要选择核心场景画时序图,例如“用户提交订单并完成支付”

可以用文字版时序图表示:

1
2
3
4
5
6
7
8
9
10
User -> OrderBuilder: create order
OrderBuilder -> Order: build(user, payment, discount, notifiers)
User -> Order: add_item(product, quantity)
User -> Order: checkout()
Order -> OrderItem: subtotal()
Order -> DiscountStrategy: calculate(raw_total)
Order -> PaymentStrategy: pay(final_total)
PaymentStrategy --> Order: payment result
Order -> OrderStateMachine: PENDING -> PAID
Order -> Notifier*: send(user, "payment success")

这一步经常能发现前面设计的问题

例如:

  • Order 支付时拿不到 PaymentStrategy,说明依赖没有注入进来
  • Notifier 需要用户联系方式,但 User 没有暴露必要信息,说明职责不完整
  • OrderItem 需要访问库存,说明边界可能没有划清,或者库存不该出现在该流程
  • 状态可以从 PENDING 直接到 COMPLETED,说明状态流转缺少约束

如果时序图走不通,不要硬写代码,而要回到职责和类图重新调整

第七步:代码落地与迭代

第七步才把设计翻译成代码

C++ 版本可以用 std::unique_ptr 表达策略对象的独占所有权,用 enum class 表达订单状态,用异常表达非法状态跳转。

下面是一段压缩后的代码骨架,重点展示“分析如何落到代码”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

enum class OrderStatus {
Pending,
Paid,
Shipped,
Completed,
Cancelled
};

struct Product {
int id;
std::string name;
double price;
};

class User {
public:
User(std::string name, std::string contact)
: name_(std::move(name)), contact_(std::move(contact)) {}

const std::string& name() const { return name_; }
const std::string& contact() const { return contact_; }

private:
std::string name_;
std::string contact_;
};

class OrderItem {
public:
OrderItem(Product product, int quantity)
: product_(std::move(product)), quantity_(quantity) {
if (quantity <= 0) {
throw std::invalid_argument("quantity must be positive");
}
}

double subtotal() const {
return product_.price * quantity_;
}

private:
Product product_;
int quantity_;
};

class PaymentStrategy {
public:
virtual std::string pay(double amount) = 0;
virtual ~PaymentStrategy() = default;
};

class DiscountStrategy {
public:
virtual double calculate(double raw_total) const = 0;
virtual ~DiscountStrategy() = default;
};

class Notifier {
public:
virtual void send(const User& user, const std::string& message) = 0;
virtual ~Notifier() = default;
};

class OrderStateMachine {
public:
explicit OrderStateMachine(OrderStatus status) : status_(status) {}

void transition_to(OrderStatus next) {
if (!valid(status_, next)) {
throw std::logic_error("invalid order status transition");
}
status_ = next;
}

OrderStatus current() const {
return status_;
}

private:
static bool valid(OrderStatus from, OrderStatus to) {
if (from == OrderStatus::Pending) {
return to == OrderStatus::Paid || to == OrderStatus::Cancelled;
}
if (from == OrderStatus::Paid) {
return to == OrderStatus::Shipped || to == OrderStatus::Cancelled;
}
if (from == OrderStatus::Shipped) {
return to == OrderStatus::Completed;
}
return false;
}

OrderStatus status_;
};

class Order {
public:
Order(
const User& user,
std::unique_ptr<PaymentStrategy> payment,
std::unique_ptr<DiscountStrategy> discount,
std::vector<std::unique_ptr<Notifier>> notifiers)
: user_(user),
payment_(std::move(payment)),
discount_(std::move(discount)),
notifiers_(std::move(notifiers)),
state_(OrderStatus::Pending) {}

void add_item(Product product, int quantity) {
if (state_.current() != OrderStatus::Pending) {
throw std::logic_error("cannot add item after checkout");
}
items_.emplace_back(std::move(product), quantity);
}

double raw_total() const {
double sum = 0.0;
for (const auto& item : items_) {
sum += item.subtotal();
}
return sum;
}

double final_total() const {
return discount_->calculate(raw_total());
}

void checkout() {
if (items_.empty()) {
throw std::logic_error("empty order cannot be checked out");
}

const double amount = final_total();
const std::string result = payment_->pay(amount);
state_.transition_to(OrderStatus::Paid);
notify_all(result);
}

void ship() {
state_.transition_to(OrderStatus::Shipped);
notify_all("order shipped");
}

void complete() {
state_.transition_to(OrderStatus::Completed);
notify_all("order completed");
}

private:
void notify_all(const std::string& message) {
for (const auto& notifier : notifiers_) {
notifier->send(user_, message);
}
}

const User& user_;
std::vector<OrderItem> items_;
std::unique_ptr<PaymentStrategy> payment_;
std::unique_ptr<DiscountStrategy> discount_;
std::vector<std::unique_ptr<Notifier>> notifiers_;
OrderStateMachine state_;
};

这段代码对应前面的设计:

  • OrderItem::subtotal() 来自“订单项知道商品和数量,能算本行小计”
  • PaymentStrategy 来自“支付渠道是变化点”
  • DiscountStrategy 来自“折扣规则是变化点”
  • Notifier 列表来自“支付成功后多渠道通知”
  • OrderStateMachine 来自“订单状态单向流转不可逆”
  • Order::checkout() 对应时序图中的核心交互

设计判断

为什么支付、折扣、通知不用写死在 Order

如果把支付写成这样:

1
2
3
4
5
if (type == PaymentType::Alipay) {
// alipay
} else if (type == PaymentType::Wechat) {
// wechat
}

短期很简单,但每增加一个支付渠道都要改 OrderOrder 会越来越大,也越来越容易被改坏

策略模式把“新增支付渠道”变成“新增一个实现类”,核心订单流程不需要修改,符合开闭原则

为什么 Product 不需要多态

商品当然可以分很多类型,但本案例只要求商品有名称和单价,订单项只需要用单价计算小计

此时给 Product 设计继承树没有收益,过早抽象会增加阅读成本,也会让后续代码更难测试

为什么状态流转要单独约束

订单状态不是普通字段,不能随便赋值

Pending -> Paid -> Shipped -> Completed 是业务规则,如果把状态暴露成 set_status(),外部代码就可能让订单从 Pending 直接变成 Completed。状态机的作用是把非法状态变化挡在对象内部

为什么 AI 辅助编码前要先分析设计

现在 AI 已经可以生成大量代码,但如果上来就让它写完整系统,复杂性会失控。更好的流程是:

  1. 先让 AI 按 OOD 七步生成分析文档
  2. 人阅读并修改边界、对象、职责和交互
  3. 类图、时序图和状态流转确认后,再让 AI 生成代码
  4. 人继续审查代码是否忠实实现设计

这样 AI 是执行和辅助推理工具,不是随意吐代码的机器,人必须保持主导,否则最后拿到几千行代码,反而要花更长时间理解它为什么这么写

$ discussion
# Comments
waline