过度设计的代价
有时,我们把系统做得如此灵活,最后却没人能理解它,连自己也不例外。我犯过这种错,次数多得不太愿意承认。
事情通常始于好意。“如果我们需要支持多种数据库呢?”“如果需求变了呢?”“如果用户增长到几百万呢?”
于是,我们添加抽象层、配置选项、插件系统。不知不觉间,实现一个简单功能也需要三天,因为我们必须先穿过自己建造的迷宫。
抽象的陷阱
抽象很有用,但它有代价。每增加一层,就多了一层需要别人理解的东西。
// 简单清晰
function getUser(id) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
// 过度设计
function getUser(id) {
return EntityManager.getInstance()
.getRepository('User')
.createQueryBuilder()
.withStrategy(new EagerLoadingStrategy())
.where({ id })
.useCache(CacheConfig.default)
.execute();
}
第二个版本也许“更灵活”,但你应该问问自己:真的需要这种灵活性吗?还是说,你正在解决尚不存在的问题?
YAGNI:你不会需要它
我学到的最有价值的原则之一是 YAGNI,也就是 You Aren't Gonna Need It。它提醒我们,想象中会用到的功能,很少与现实完全一致。
我曾花两周时间,为一个项目构建插件系统。它支持动态加载、版本管理、沙盒执行,该有的都有。你猜最后我们创建了多少个插件?零个。
这两周原本可以用来开发用户真正需要的功能。
过度设计的迹象
留意下面这些情况:
- 你正在解决假设中的问题。 “如果有一天……”是一个值得警惕的信号。
- 简单修改也需要动很多文件。 抽象应该减少复杂性,而不是把它分散到各处。
- 你需要靠文档才能理解自己的代码。 如果上个月写的代码,今天已经读不明白,那它就太复杂了。
- 新成员很难参与开发。 如果入门需要数周,问题可能出在架构上。
- 你为代码有多巧妙而自豪。 巧妙的代码往往也很脆弱。
代价确实存在
过度设计并非没有成本。它消耗的是:
- 开发时间。 构建抽象层往往比实现功能更耗时。
- 维护精力。 代码越多,缺陷、更新工作和理解负担也越多。
- 团队效率。 复杂的系统会拖慢每个人的速度。
- 机会成本。 用在架构上的时间,就无法再用来服务用户。
什么时候值得抽象
我不是说永远不要抽象。抽象很重要,但它应该来自真实需要,而不是事先假定。
值得抽象的理由包括:
- 相同的代码已经写了三次,也就是“三次法则”
- 模式已经稳定,而且大家都理解它
- 抽象带来的简化多于它引入的复杂性
- 你有具体、当前的需求,而不是假设中的未来需求
// 有真实需求支撑的抽象:我们确实有多个数据源
function fetchData(source, query) {
const adapter = adapters[source]; // postgres, mongodb, api
return adapter.fetch(query);
}
简单的设计更经得起增长
有一个看似反直觉的事实:简单的代码,往往比巧妙的代码更容易扩展。
出了问题时,简单的代码更容易调试。性能变得重要时,简单的代码更容易优化。需求发生变化时,简单的代码更容易重构。
我参与过的那些最容易维护的系统,架构并不惊艳。它们甚至有些平淡,但行为可预测,也容易理解。
而这恰恰让它们可靠。
给自己一个挑战
下次你想再加一层抽象时,先问问自己:
- 我今天究竟在解决什么具体问题?
- 这是能够奏效的最简单方案吗?
- 未来的我,或者新加入的同事,能理解它吗?
如果无法自信地回答,考虑选择更简单的做法。以后总能增加复杂性,但要去掉它,就困难得多了。
为今天构建,为明天重构。记住:清晰的设计,比巧妙的设计更经得起规模增长。
模板示例