第15章:SOLID原则总结与对比——五大原则的内在联系,如何在实际项目中综合运用
说实话,讲到这里,SOLID的五个原则我们都已经逐个聊过了。但有个问题我一直想强调——这五个原则不是孤立的。你想想看,它们就像五个手指,各有各的用处,但握成拳头才有力气。
我在项目中见过不少团队,把SOLID当成教条来背。有人死磕单一职责,结果类拆得比芝麻还碎;有人迷恋开闭原则,接口设计得跟俄罗斯套娃似的。嗯,这其实走偏了。
今天我们就来做个总结。把五个原则串起来,看看它们到底怎么配合,怎么在实际项目中落地。
一、SOLID原则的内在联系
先画张图,让你一眼看明白它们的关系。
从这张图你能看到什么?我来说说我的理解。
SRP 和 OCP 是基础。一个类只做一件事,你才有底气去扩展它。我在重构一个支付模块时深有体会——原来一个 PaymentService 既管验签又管记账又管通知,想加个新支付方式,改代码改到崩溃。拆成单一职责后,加新渠道只需要写个新类,完全符合 OCP。
LSP 是 OCP 的保障。你开放扩展了,但子类不能乱来。我见过有人继承了一个基类,重写方法时直接抛异常——这其实就违反了 LSP。调用方根本不敢用多态,OCP 也就成了空话。
ISP 和 DIP 是解耦的利器。接口别太胖,依赖别太具体。这两个原则配合起来,你的系统才能做到「高内聚、低耦合」。
二、五个原则的对比
为了方便你记忆,我整理了一张表。你写代码时拿不准,可以回来翻翻。
| 原则 | 核心思想 | 关注点 | 违反时的症状 | 我常用的落地手法 |
|---|---|---|---|---|
| SRP | 一个类只有一个变更理由 | 类的职责边界 | 一个类改来改去,每次原因不同 | 先写职责描述,超过一句话就拆 |
| OCP | 对扩展开放,对修改关闭 | 系统的可扩展性 | 加功能就要改老代码 | 抽象接口 + 策略模式 |
| LSP | 子类必须能替换父类 | 继承的正确性 | 多态调用时行为异常 | 子类不削弱父类的前置条件 |
| ISP | 接口要小而专 | 接口的粒度 | 实现类被迫实现无用方法 | 按调用方拆分接口 |
| DIP | 依赖抽象,不依赖具体 | 模块间的耦合 | 高层模块直接依赖底层细节 | 依赖注入 + 工厂模式 |
三、实际项目中如何综合运用
光讲理论没意思。我拿一个真实项目来举例——订单处理系统。这个场景几乎每个做后端的人都会遇到。
3.1 场景描述
假设我们要设计一个订单处理模块。订单有多种类型:普通订单、会员订单、促销订单。每种订单的计价、优惠、积分计算都不一样。
你想想看,如果不用 SOLID,你会怎么写?大概率是一个 OrderService 类,里面塞满 if-else。嗯,我早期就是这么干的,结果维护了三个月就想跑路。
3.2 用 SOLID 重构
我们一步步来。
第一步:SRP 拆分职责
先把大胖子拆开。订单处理至少包含:
- 订单数据管理(OrderRepository)
- 价格计算(PriceCalculator)
- 优惠策略(DiscountStrategy)
- 积分处理(PointsHandler)
- 通知发送(NotificationService)
每个类只干一件事。这样改起来,你不用担心牵一发动全身。
第二步:OCP + 策略模式
不同类型的订单,计价和优惠逻辑不同。我们用策略模式来处理。
// 抽象策略接口
class IDiscountStrategy {
public:
virtual double calculate(double price) = 0;
virtual ~IDiscountStrategy() = default;
};
// 具体策略
class NormalDiscount : public IDiscountStrategy {
public:
double calculate(double price) override {
return price; // 无优惠
}
};
class MemberDiscount : public IDiscountStrategy {
public:
double calculate(double price) override {
return price * 0.85; // 会员85折
}
};
class PromoDiscount : public IDiscountStrategy {
public:
double calculate(double price) override {
return price > 100 ? price - 20 : price; // 满100减20
}
};
加新类型?写个新策略类就行。老代码不用动。这就是 OCP 的威力。
第三步:LSP 保证安全
写子类时要注意。比如你写一个 VIPMemberDiscount 继承 MemberDiscount,重写 calculate 时不能把折扣改成 1.2 倍——这违反了 LSP。调用方预期是打折,结果你给涨价了,业务逻辑全乱套。
第四步:ISP 精简接口
别搞一个大而全的 IOrderProcessor 接口。不同的调用方只需要不同的能力:
// 按职责拆分接口
class IPriceCalculable {
public:
virtual double calculatePrice() = 0;
};
class IDiscountable {
public:
virtual double applyDiscount(double price) = 0;
};
class IPointsEarnable {
public:
virtual int calculatePoints(double amount) = 0;
};
这样,一个订单类只需要实现它需要的接口。不会出现「我明明不需要积分,却被迫实现一个空的 calculatePoints」的尴尬。
第五步:DIP 解耦依赖
高层模块(比如 OrderService)不要直接 new 具体的策略类。用依赖注入:
class OrderService {
public:
// 依赖抽象,不依赖具体
OrderService(std::shared_ptr<IDiscountStrategy> strategy,
std::shared_ptr<IPointsEarnable> pointsHandler)
: strategy_(strategy), pointsHandler_(pointsHandler) {}
void processOrder(Order& order) {
double finalPrice = strategy_->calculate(order.getPrice());
int points = pointsHandler_->calculatePoints(finalPrice);
// ... 处理订单
}
private:
std::shared_ptr<IDiscountStrategy> strategy_;
std::shared_ptr<IPointsEarnable> pointsHandler_;
};
这样,OrderService 根本不关心你用的是哪种折扣策略。换策略?换个注入对象就行。
四、综合运用的节奏感
你可能要问了:每个项目都这么搞,是不是太复杂了?
我的建议是:别过度设计。SOLID 是工具,不是枷锁。
- 小项目、原型阶段:先跑起来再说。等代码开始「发臭」了再重构。
- 核心模块、频繁变动的部分:优先应用 SOLID。这部分值得投入。
- 团队协作的项目:接口和抽象要提前定好。不然每个人写出来的风格都不一样,后期合并代码会想哭。
五、总结
SOLID 五个原则,说白了就是一件事:让代码更容易被修改。
SRP 让你知道改哪里,OCP 让你不改老代码,LSP 让你放心改,ISP 让你少改,DIP 让你改的时候不影响别人。
我在项目中见过太多「不敢改代码」的团队。一改就崩,一崩就加班,一加班就骂娘。其实根源就是设计没做好。SOLID 不能解决所有问题,但它能帮你把系统的「脆弱性」降到最低。
嗯,这一章就到这里。记住:原则是死的,人是活的。理解背后的思想,比背下五个名字重要得多。
公众号:蓝海资料掘金营,微信 deep3321