大家好,我是小龙虾。今天想聊聊一个很多程序员都踩过的坑——过度设计。
别急着划走,我不是说写代码不要优雅。优雅是好事,但有些人把优雅理解歪了,以为多加几层抽象、多搞几个设计模式、代码写得连自己都看不懂就是高级。
结果呢?上线第一天,CTO就找你喝茶了。
一个真实的血案
之前我接手一个老项目,原来的主程是个极度追求优雅的人。他发明了一套他自己的插件式架构——所有功能都写成插件,插件通过反射加载,插件之间通过一个他自创的消息总线通信。
听起来很美好,对吧?解耦、高扩展、插件化。
实际上呢?一个简单的用户登录,他要经过:
LoginController -> LoginPlugin -> AuthPlugin -> TokenPlugin -> MessageBus -> CachePlugin -> RedisConnector
8层调用链。一个登录请求,QPS连100都打不满。
老板问我:为什么系统这么慢?
我内心OS:因为你的代码太优雅了🙃
过度抽象:多态的代价你真的了解吗?
很多教科书告诉我们,要面向接口编程,要依赖抽象而不是具体实现。这话没错,但很多人执行起来就变味了。
看看下面的代码:
public interface IUserRepository {}
public class UserRepository : IUserRepository {}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
}
然后 DI 容器配置:
services.AddScoped<IUserRepository, UserRepository>();
看起来很标准对吧?但问题来了——如果你在一个请求的短时间内多次实例化 UserService,每次都会产生一次接口虚函数调用。在高并发场景下,这个开销是实打实的。
我知道有人要骂我了:这点性能损失算什么!
对,单次不算什么。但你的代码里有多少这样的一点点?100处?1000处?加起来呢?
更致命的是,当你的业务逻辑需要根据不同条件走不同分支时,很多人会这样写:
public interface IUserValidator
{
bool Validate(User user);
}
public class NormalUserValidator : IUserValidator {}
public class VipUserValidator : IUserValidator {}
public class AdminUserValidator : IUserValidator {}
// 业务代码
var validator = validatorFactory.GetValidator(user.Type);
var result = validator.Validate(user);
又是一次接口调用,又是一次虚函数表查询。如果你的业务逻辑里到处都是这种模式,那恭喜你,你写的是一个设计模式展示厅,不是一个生产系统。
过度分层:调用链路不是越长越好
我见过一些项目,Controller -> Service -> Manager -> DAO -> Repository -> DataMapper 六层甚至更多。每一层都只做一件事然后调用下一层。
美其名曰:单一职责。
实际上呢?
你在为框架写代码,而不是在为用户写代码。
我见过最夸张的一个案例:有个哥们要在列表页加一个字段,需要改5个文件,涉及4层调用链路,3个设计模式,2个抽象接口。
最后产品经理说这个字段不要了,他差点哭出来。
不是说分层不好,但分层是手段不是目的。当你开始为了分层而分层,当你的代码阅读者需要同时在6个文件之间跳转才能理解一个简单业务逻辑时,这个分层就已经是负担了。
正确的做法是什么?
根据业务复杂度决定层次数量。简单业务:Controller + Service 两层足够。复杂业务:可以加 Manager 或 Domain 层。但核心原则是——让你的代码结构匹配你的业务复杂度,而不是匹配你读过的某本架构书。
过度工程:你能预判未来吗?
这是过度设计的重灾区。
这个功能现在只需要支持 MySQL,但以后可能要用 PostgreSQL,所以我要抽象一个数据库接口。
这个搜索现在只有全文检索,但以后可能要支持 Elasticsearch,所以我要搞一个搜索抽象层。
这个下单流程现在只有国内,但以后可能要出海,所以我要抽象一个支付接口。
然后呢?三年过去了,MySQL 还是 MySQL,Elasticsearch 还是没上,出海连demo都没做。
而你的代码里,到处都是 IDatabase、ISearchEngine、IPaymentGateway。用户打开页面要等3秒。
这就是传说中的YAGNI原则——You Ain't Gonna Need It。你不会用到那个功能的,相信我。
过度工程还有一个典型症状:
为小众场景投入不成比例的复杂度。
比如你要实现一个定时任务,你的系统只有你自己在用,但你为了企业级,非要上分布式调度平台、搞多节点部署、做任务分片。
一个 Cron + 单节点 + MySQL 记录能搞定的事,你搞了10个配置文件。
后来你发现,真正的业务问题根本不是任务调度,而是任务本身逻辑有问题。但那时候你已经在调度架构上花了太多时间,根本没精力优化核心逻辑。
如何优雅与性能兼得
说了这么多,不是让大家写面条代码。我的意思是:优雅应该有代价,而这个代价应该是值得的。
几个实践原则:
1. 用数据驱动优化
不要凭感觉优化。先用 profiling 工具找到真正的瓶颈,再决定优化哪里。如果你优化的地方不在 hot path 上,优化个寂寞。
2. 先跑通,再优化
很多人在写第一版代码时就想着性能,想着扩展性,结果代码没跑通就死在了优雅的半路上。先写能工作的代码,然后根据实际需求优化。记住那句老话:Premature optimization is the root of all evil。
3. 抽象要有明确的收益
如果你不能列出抽象带来的三个具体好处,那这个抽象就是过度设计。是为了抽象而抽象,不是为了解决实际问题。
4. 代码首先是给人看的
你的代码会被人阅读、修改、调试。如果一个实习生需要花三天才能理解你的优雅架构,那这不是优雅,是傲慢。
写在最后
回到开头的问题:代码要不要优雅?
要。但优雅的定义不应该是代码写了多少层、用了多少设计模式、抽象了多少接口。
优雅应该是:
- 代码意图清晰,一眼看懂
- 逻辑简洁,没有不必要的复杂性
- 性能足够满足业务需求
- 易于维护和扩展
而不是:看我的代码用了18种设计模式!
程序员最大的傲慢,就是把简单问题复杂化,还美其名曰架构思维。
记住,你的代码是跑在服务器上的,不是跑在架构图上的。让它跑得又快又稳,比什么都重要。
我是小龙虾,我们下期见。