第46讲:设计原则的权衡——过度设计的陷阱,YAGNI原则,KISS原则与设计原则的平衡
说实话,我见过太多团队把设计原则当成了“圣旨”。
一上来就想着“我要高内聚低耦合”、“我要开闭原则”、“我要依赖倒置”。结果呢?代码写了一堆抽象层,接口套接口,最后连自己都绕晕了。项目延期,需求变更,那套“完美”的设计反而成了最大的累赘。
这一讲,我们就来聊聊设计原则的“度”。
1. 过度设计的陷阱:你是在设计,还是在炫技?
先讲个我自己的经历。几年前我接手过一个中间件项目,前任架构师把整个系统分成了7层抽象。每一层都有接口、虚基类、工厂模式、策略模式……代码读起来像在读《设计模式大全》。
但问题是,这个系统只处理三种消息类型。三种!
我花了三天时间,才搞清楚一条消息从收到处理,到底经过了哪些对象。后来我重构了,直接砍掉4层,用了一个简单的分发器加三个处理函数。代码量减少60%,性能提升30%。
这就是典型的过度设计。
过度设计的典型症状:
- 为未来可能出现的需求,提前预留了大量扩展点
- 抽象层次过多,调用链路过长
- 大量使用设计模式,但实际场景根本不需要
- 代码难以理解,新人上手成本极高
为什么会这样?说白了,很多开发者把“设计”和“写复杂代码”划了等号。觉得不用几个设计模式,就不够“专业”。
但真正的专业,是用最简单的方式解决问题。
2. YAGNI原则:你不会需要它
YAGNI,全称是“You Aren't Gonna Need It”。翻译过来就是:你根本用不着它。
这个原则来自极限编程(XP),核心思想很简单:只实现当前明确需要的功能,不要为未来可能的需求提前写代码。
我的习惯:每次想加一个抽象层或接口时,先问自己三个问题:
- 这个功能现在就需要吗?
- 如果现在不做,以后加的成本会很高吗?
- 这个抽象真的能应对未来的变化吗?
如果前两个答案是否定的,第三个不确定,那就别做。
举个例子。假设你正在写一个日志模块,目前只输出到文件。你心想:“以后可能要输出到网络、数据库,我得先抽象一个LogSink接口。”
于是你写了:
class LogSink {
public:
virtual void write(const std::string& msg) = 0;
virtual ~LogSink() = default;
};
class FileSink : public LogSink {
// 实现...
};
class Logger {
std::unique_ptr<LogSink> sink_;
public:
Logger(std::unique_ptr<LogSink> sink) : sink_(std::move(sink)) {}
void log(const std::string& msg) { sink_->write(msg); }
};
看起来挺“优雅”对吧?但问题是,项目跑了一年,需求从来没变过。你那个LogSink接口,唯一的实现就是FileSink。多出来的抽象层,除了增加代码量和理解成本,没有任何价值。
注意:YAGNI不是让你不做设计。而是让你延迟决策。当真正需要多态时,再引入接口也不迟。重构的成本,往往远低于过度设计的成本。
3. KISS原则:保持简单和直接
KISS,Keep It Simple, Stupid。别笑,这个“Stupid”不是骂人,而是提醒你:把代码写得简单到“傻瓜”都能看懂。
我个人认为,KISS是比所有设计原则都更重要的原则。因为简单意味着:
- 容易理解
- 容易修改
- 容易测试
- 不容易出错
来看一个对比。假设你要实现一个功能:根据用户角色返回不同的菜单列表。
复杂版本(过度设计):
class MenuProvider {
public:
virtual std::vector<MenuItem> getMenu(const User& user) = 0;
};
class AdminMenuProvider : public MenuProvider {
std::vector<MenuItem> getMenu(const User& user) override {
return { /* 管理员菜单 */ };
}
};
class NormalMenuProvider : public MenuProvider {
std::vector<MenuItem> getMenu(const User& user) override {
return { /* 普通用户菜单 */ };
}
};
class MenuService {
std::map<UserRole, std::unique_ptr<MenuProvider>> providers_;
public:
MenuService() {
providers_[UserRole::Admin] = std::make_unique<AdminMenuProvider>();
providers_[UserRole::Normal] = std::make_unique<NormalMenuProvider>();
}
std::vector<MenuItem> getMenu(const User& user) {
return providers_[user.getRole()]->getMenu(user);
}
};
简单版本(KISS):
std::vector<MenuItem> getMenuForUser(const User& user) {
switch (user.getRole()) {
case UserRole::Admin: return { /* 管理员菜单 */ };
case UserRole::Normal: return { /* 普通用户菜单 */ };
default: return {};
}
}
哪个好?我选第二个。三个角色?加个case。五个角色?还是加case。直到switch变得臃肿,再考虑重构。别提前“优化”。
4. 设计原则的平衡:什么时候该用,什么时候该收?
你可能会问:“那设计原则到底还要不要学?”
当然要学。但你要明白:设计原则是工具,不是目的。
我总结了一个简单的判断框架:
| 场景 | 建议 | 理由 |
|---|---|---|
| 代码会被多次复用 | 适当抽象,遵循开闭原则 | 减少未来修改的波及范围 |
| 需求明确且稳定 | KISS,直接实现 | 避免过度设计 |
| 需求不确定,变化频繁 | YAGNI + 简单设计 | 等需求明确了再重构 |
| 团队规模大,多人协作 | 适当增加接口和文档 | 降低沟通成本 |
| 核心模块,性能敏感 | 优先考虑性能,减少抽象 | 避免虚函数等开销 |
嗯,这里要注意:没有银弹。每个项目、每个模块的平衡点都不一样。我个人的经验是:先写一个能跑的版本,然后根据实际需求逐步重构。别一开始就想着“完美”。
5. 避坑指南:我曾经踩过的坑
我曾经在一个支付系统中,为了“优雅”地处理多种支付方式,设计了一个复杂的策略模式+工厂模式+注册表。结果呢?
- 新加一种支付方式,要改三个文件
- 调试时,调用链路过长,根本不知道哪里出了问题
- 后来需求变了,支付方式从5种减到2种,那套“优雅”的设计瞬间变成了累赘
最后我全删了,换成简单的if-else。代码量从800行降到150行。测试覆盖率从60%升到95%。
记住:最好的设计,是当你半年后回头看自己的代码时,能在一分钟内看懂它在干什么。而不是感叹“我当时真牛逼,写了这么复杂的架构”。
6. 知识体系:一张图看懂权衡
下面这张图,是我对设计原则权衡的理解。你可以把它当作一个决策参考:
7. 总结
设计原则是经验,不是教条。YAGNI告诉你别做多余的事,KISS告诉你要简单,而其他设计原则告诉你怎么在需要的时候组织代码。
这三者并不矛盾。它们的关系是:YAGNI和KISS是“刹车”,其他设计原则是“油门”。只会踩油门,车会翻;只会踩刹车,车不走。
我建议你:
- 写代码前,先问自己“这个抽象现在真的需要吗?”
- 写完后,看看能不能删掉一半的代码还能正常工作
- 如果半年后你还能看懂自己的代码,那就是好设计
最后一句:别让设计原则成为你偷懒的借口。真正的架构师,懂得在合适的时候说“不”,也懂得在合适的时候说“够了”。
公众号:蓝海资料掘金营,微信deep3321