程序员10年成长记:第9篇:从“搬砖”到“砌墙”——如何写出“可扩展”的代码
引言
时间来到了2019年的春节,整个中国都沉浸在一种复杂的情绪中。一方面,中美贸易摩擦的阴云让经济前景变得扑朔迷离;另一方面,一部名为《流浪地球》的科幻电影点燃了所有人的激情,它讲述了一个带着地球去“流浪”的宏大故事。
“带着地球去流浪”,这几个字深深触动了启明科技的CEO。
公司的“凤凰项目”(单体电商)在2018年的“新零售”战役中,已经显现出“尾大不掉”的疲态。而拼多多的异军突起(2018年7月上市),更是让CEO焦虑万分,他意识到“社交+电商”的巨大威力。
在一次高层战略会上,CEO拍板决定,效仿“流浪地球”,启动一个代号为**“银河计划”(Galaxy Project)**的宏大工程:抛弃陈旧的“凤凰”单体架构,全面转向“云原生”微服务。
这个计划的核心,是在上海新成立一个“研发中心”,轻装上阵,用全新的技术栈(Spring Cloud, Docker, Kubernetes, Redis, MQ)从零开始重构公司的核心业务。
小葵,凭借在“幽灵库存”事件和“复盘”中的出色表现,被老李力荐,成为了“银河计划”的先遣队成员。她告别了北京熟悉的老团队,独自一人来到上海,她的职级也从“工程师”晋升为“资深工程师”。
而张三,则选择留在了北京,继续维护“凤凰”单体项目。他觉得:“上海房价那么贵,K8s那么复杂,都是虚的,把Bug改了才是真的。”
小葵和张三的职业轨迹,在这一刻,正式拉开了第一个显著的岔口。
小故事:“银河”的引力与“凤凰”的“补丁”
- 新的“战场”,新的“需求”
小葵在上海入职的第一天,领到了一台全新的MacBook Pro和一张“银河计划”的架构图。她的第一个任务,就是构建“银河系”的“恒星”—— **“用户中心”(user-service)**微服务。
几乎在同一时间,CEO在产品评审会上,受“拼多多”刺激,提出了一个紧急需求:
“我们必须立刻支持社交登录!给两个月时间,App要上线**‘微信登录’和‘拼团匿名登录’**(一种临时的访客账户)!”
这个需求,同时发给了北京的张三和上海的小葵。
-
北京(旧战场): 张三的任务是,在“凤凰”单体项目上,打个补丁,实现功能。
-
上海(新战场): 小葵的任务是,在“用户中心”微服务上,构建一个可扩展的登录体系。
-
张三的“If-Else”补丁
张三打开了“凤凰”项目中那数万行的
UserService.java,找到了一个叫login()的方法。他深吸一口气,他最擅长的就是“修补”。他决定在
UserController里新建一个loginV2()接口,然后开始了他的“艺术创作”:// 张三的代码 (位于 UserService.java) public UserInfo loginV2(LoginRequest request) { String type = request.getLoginType(); if ("PASSWORD".equals(type)) { // 1. 检查用户名密码 // 2. 校验验证码 // 3. 登录成功,返回用户信息 // ... (省略50行代码) ... return userInfo; } else if ("WECHAT".equals(type)) { // 1. 调用微信API,用code换取openId String openId = wechatApiService.getOpenId(request.getCode()); // 2. 检查openId是否已绑定 User user = userDao.findByOpenId(openId); if (user != null) { // 3. 已绑定,直接登录 return buildToken(user); } else { // 4. 未绑定,检查手机号是否存在 User phoneUser = userDao.findByPhone(request.getPhone()); if (phoneUser != null) { // 5. 手机号存在,绑定微信 phoneUser.setOpenId(openId); userDao.update(phoneUser); return buildToken(phoneUser); } else { // 6. 手机号不存在,创建新用户 User newUser = new User(); newUser.setPhone(request.getPhone()); newUser.setOpenId(openId); // ... (省略20行) ... userDao.insert(newUser); return buildToken(newUser); } } } else if ("GUEST".equals(type)) { // 1. 创建一个匿名访客用户 // 2. 设置一个较短的Token有效期 // ... (省略30行代码) ... return guestInfo; } return null; // 理论上不会到这里 }张三花了两天时间,写了近200行代码,终于调通了。他长舒一口气:“搞定,功能实现了。”
-
小葵的“可扩展”砌墙
小葵在上海的白板前,思考的不是“如何实现微信登录”,而是:“未来一定还会有QQ登录、抖音登录、微博登录……我如何设计一个系统,让‘增加一种新登录方式’变得像‘插拔U盘’一样简单?”
这,就是“资深工程师”和“工程师”的思维差异。
她想起了《Head First 设计模式》中的“策略模式”。她决定用“开闭原则”来武装自己的微服务。
第一步:定义一个“策略”接口 (LoginStrategy)
// 登录策略接口 public interface LoginStrategy { /** * 登录处理 * @param request 登录请求 * @return 用户信息 */ UserInfo login(LoginRequest request); /** * 返回该策略支持的登录类型 * @return 登录类型标识,如 "PASSWORD", "WECHAT" */ String getStrategyName(); }第二步:创建具体的“策略”实现 (Implementations)
她利用Spring Boot的特性,将每个策略实现为一个
@Component。@Component public class PasswordLoginStrategy implements LoginStrategy { @Override public UserInfo login(LoginRequest request) { // 1. 检查用户名密码 // ... (省略50行代码) ... return userInfo; } @Override public String getStrategyName() { return "PASSWORD"; } } @Component public class WechatLoginStrategy implements LoginStrategy { @Autowired private WechatApiService wechatApiService; @Autowired private UserDao userDao; @Override public UserInfo login(LoginRequest request) { // 1. 调用微信API,用code换取openId // ... (这里是张三写的 100 行嵌套if-else逻辑) ... // ... 重点是,所有微信登录的复杂性,都被封装在了这里 ... return userInfo; } @Override public String getStrategyName() { return "WECHAT"; } } @Component public class GuestLoginStrategy implements LoginStrategy { // ... 访客登录的逻辑 ... @Override public String getStrategyName() { return "GUEST"; } @Override public UserInfo login(LoginRequest request) { /* ... */ } }第三步:创建“策略工厂” (LoginStrategyFactory)
小葵利用Spring的依赖注入,巧妙地构建了一个“工厂”,它在启动时会自动“收集”所有
LoginStrategy的实现。@Component public class LoginStrategyFactory implements InitializingBean { // Spring的Bean初始化接口 @Autowired private List<LoginStrategy> strategies; // Spring会注入所有LoginStrategy的实现 private Map<String, LoginStrategy> strategyMap; /** * 在Spring Bean初始化后,自动构建一个Map */ @Override public void afterPropertiesSet() throws Exception { strategyMap = strategies.stream() .collect(Collectors.toMap(LoginStrategy::getStrategyName, s -> s)); } /** * 根据类型获取对应的策略 */ public LoginStrategy getStrategy(String type) { LoginStrategy strategy = strategyMap.get(type); if (strategy == null) { throw new UnsupportedOperationException("不支持的登录类型: " + type); } return strategy; } }第四步:重构“用户服务” (UserService)
现在,小葵的
UserService变得异常简洁和稳定:@Service public class UserServiceImpl implements UserService { @Autowired private LoginStrategyFactory factory; @Override public UserInfo login(LoginRequest request) { // 1. 从工厂获取对应的策略 LoginStrategy strategy = factory.getStrategy(request.getLoginType()); // 2. 执行策略 // 核心:这里不关心到底是哪种登录,只管调用接口 return strategy.login(request); } } -
“Kicker”——新需求的“拷问”
一个月后,产品总监(小红)在“银河计划”的周会上,果然提出了新需求:“‘抖音’最近太火了,我们要立刻上**‘抖音登录’**!下周就要!”
-
北京(张三):
-
张三的脸都绿了。他战战兢兢地打开那个200行的loginV2方法,在GUEST的else if下面,又加了一个else if (“DOUYIN”.equals(type)) { … }。
-
在添加新逻辑时,他不小心碰到了
WECHAT分支里一个变量,导致微信登录在测试环境直接NPE(空指针)。 -
他花了整整两天时间,在“屎山”上“雕花”,终于在周五晚上9点提交了代码,并附上了一句:“修改了核心登录方法,请测试同学帮忙回归所有登录场景!”
-
上海(小葵):
-
小葵微微一笑。她只做了三件事:
-
新建一个类
DouyinLoginStrategy.java,实现LoginStrategy接口,在里面写好抖音登录的逻辑。 -
@Component标注该类。 -
提交代码。
-
-
UserService?一行代码都没改。
-
LoginStrategyFactory?一行代码都没改。
-
她只花了半天时间就完成了开发和自测。她的测试报告是:“新增抖音登录,不影响任何原有登录逻辑,已通过单元测试。”
对比:
张三是在“搬砖”,一块一块往上堆,代码越来越臃F肿,风险越来越高。
小葵是在“砌墙”,她定义了“砖块”(LoginStrategy接口)的“标准”,她搭建了“脚手架”(LoginStrategyFactory)。未来加功能,只是按标准生产“砖块”,然后放上去就行了。
张三的代码,是“功能的堆砌”;小葵的代码,是“可扩展的设计”。
核心要点:告别“功能堆砌”,拥抱“系统设计”
从小葵和张三的对比中,我们看到了“资深”与“初级”最核心的区别:
-
初级(功能思维): 拿到需求,思考“我该如何实现它?”。(如张三,用
if-else实现了功能) -
资深(系统思维): 拿到需求,思考“这是‘哪一类’问题?我该如何设计一个‘系统’,让‘这一类’所有问题都能被优雅地解决?”。(如小葵,把“登录”抽象为“策略”)
这种“系统思维”的基石,就是“高内聚、低耦合”的设计思想。
理论基础:“高内聚、低耦合”的设计灵魂
这是软件工程中被提及次数最多,但最难做到的原则。
-
高内聚 (High Cohesion):
-
定义: 把“相关”的功能,“内聚”到一个模块(类、方法)中。一个模块只做“一件事”,并且把“这件事”做好。
-
小葵的代码:
WechatLoginStrategy只负责微信登录,PasswordLoginStrategy只负责密码登录。它们各自的“内聚性”非常高。 -
张三的代码:
loginV2方法负责了“所有”登录。它的内聚性极低,是一个“大杂烩”。
-
-
低耦合 (Low Coupling):
-
定义: 模块与模块之间,应尽量减少“依赖”。如果必须依赖,也应该依赖“抽象”(接口),而不是“具体”(实现)。
-
小葵的代码:
UserService根本“不认识”WechatLoginStrategy,它只“认识”LoginStrategy这个“接口”。UserService和具体的登录逻辑“解耦”了。 -
张三的代码:
loginV2方法“强耦合”了WechatApiService、UserDao以及所有登录类型的实现细节。
-
可视化对比:
graph TD
subgraph "张三的设计-高耦合, 低内聚"
A(UserService.loginV2) --> B(微信登录逻辑)
A --> C(密码登录逻辑)
A --> D(访客登录逻辑)
A --> E(抖音登录逻辑)
B --> F(WechatApiService)
C --> G(UserDao)
E --> H(DouyinApiService)
end
subgraph "小葵的设计 (低耦合, 高内聚)"
U(UserService.login) --> V(LoginStrategyFactory)
V -- "getType()" --> W(<b>LoginStrategy 接口</b>)
W <.--- X(PasswordLoginStrategy)
W <.--- Y(WechatLoginStrategy)
W <.--- Z(GuestLoginStrategy)
W <.--- Z1(DouyinLoginStrategy)
Y --> F2(WechatApiService)
Z1 --> H2(DouyinApiService)
end
style V fill:#ffc,stroke:#333
style W fill:#ffc,stroke:#333,stroke-width:2px
关键技能(一):SOLID原则 —— “可扩展”的“宪法”
“高内聚、低耦合”是“灵魂”,而SOLID原则,就是实现这一灵魂的“法律条文”。
-
S - 单一职责原则 (Single Responsibility Principle):
-
定义: 一个类只应该有一个“引起它变化”的原因。
-
应用: 小葵的
WechatLoginStrategy只因“微信登录逻辑变更”而变化。张三的loginV2方法,任何一种登录(微信、密码、抖音)的变更,都会导致它变化。
-
-
O - 开闭原则 (Open/Closed Principle):
-
定义: 软件实体(类、模块、函数)应该对“扩展”开放,对“修改”关闭。
-
应用: 这是本篇最重要的原则。
-
小葵的代码: 完美符合OCP。当“抖音登录”需求来了,她**“扩展”了系统(增加了
**DouyinLoginStrategy**类),但“关闭”**了核心代码(UserService和Factory无需修改)。 -
张三的代码: 彻底违反OCP。当“抖音登录”需求来了,他必须**“修改”**
loginV2这个核心方法。
-
-
-
L - 里氏替换原则 (Liskov Substitution Principle):
-
定义: 子类必须可以替换掉它们的父类(或接口)。
-
应用:
UserService可以无差别地使用PasswordLoginStrategy或WechatLoginStrategy来替换LoginStrategy接口,而行为(登录)符合预期。
-
-
I - 接口隔离原则 (Interface Segregation Principle):
-
定义: 不应强迫客户端依赖它们用不到的接口。
-
应用:
LoginStrategy接口很“瘦”,只有一个login和一个getStrategyName,没有强加“注册”、“登出”等其他方法。
-
-
D - 依赖倒置原则 (Dependency Inversion Principle):
-
定义: 高层模块不应依赖低层模块,两者都应依赖“抽象”。
-
应用:
UserService(高层模块)不依赖WechatLoginStrategy(低层模块),它们都依赖LoginStrategy(抽象/接口)。这也是Spring IoC/DI(控制反转/依赖注入)的核心。
关键技能(二):设计模式 —— “可扩展”的“蓝图”
SOLID是“宪法”,设计模式就是在此基础上形成的“经典判例”和“建筑蓝图”。
-
-
策略模式 (Strategy Pattern):
-
解决什么问题: 解决“做一件事有多种方式”的问题。
-
定义: 定义一系列算法(策略),将它们一个个封装起来,并使它们可以相互替换。
-
应用场景(必看):
-
登录: (如本文)密码登录、微信登录、短信登录…
-
支付: 支付宝支付、微信支付、银行卡支付…
-
校验: 不同的风控规则、不同的参数校验器…
-
路由: 不同的负载均衡算法(轮询、随机、哈希)…
-
- 工厂模式 (Factory Pattern):
-
解决什么问题: 解决“如何创建不同对象”的问题。
-
定义: 将“创建对象”的逻辑封装起来,客户端只需告诉工厂“我想要什么”,而无需关心“它是怎么被造出来的”。
-
应用场景:
-
策略工厂: (如本文)根据
type创建对应的Strategy。 -
数据库连接: 根据配置创建
MySQLConnection或OracleConnection。 -
消息队列: 根据配置创建
RocketMQProducer或KafkaProducer。
-
- 模板方法模式 (Template Method Pattern):
-
解决什么问题: 解决“一件事的流程固定,但个别步骤不同”的问题。
-
定义: 在一个抽象类中定义一个算法的“骨架”(模板),而将一些可变的步骤延迟到子类中实现。
-
应用场景:
-
举例: 假设“注册”流程是固定的:1. 校验参数 -> 2. 创建用户数据 -> 3. 发放新用户福利。
-
AbstractRegisterHandler(抽象类)定义了register()这个“模板方法”。 -
validateParams()和createUserData()是抽象的,由子类实现(微信注册和密码注册的校验逻辑不同)。 -
sendWelcomeBonus()是具体的(所有注册都送10元券)。
-
classDiagram
class AbstractRegisterHandler {
+register(request)*
#validateParams(request)$
#createUserData(request)$
#sendWelcomeBonus(userId)
}
note for AbstractRegisterHandler {
register() {
validateParams();
user = createUserData();
sendWelcomeBonus(user.id);
}
}
class WechatRegisterHandler {
+validateParams(request)
+createUserData(request)
}
class PasswordRegisterHandler {
+validateParams(request)
+createUserData(request)
}
AbstractRegisterHandler <|-- WechatRegisterHandler
AbstractRegisterHandler <|-- PasswordRegisterHandler
实战要点:在业务代码中“嗅出”模式的味道
你不需要为了用模式而用模式(过度设计),而应该在代码“变坏”时,用模式去“拯救”它。
-
代码坏味道 (Code Smell): 巨大的
if-else或switch。-
诊断: 违反了“开闭原则”和“单一职责”。
-
药方: 策略模式 或 工厂模式。
-
-
代码坏味道: 多个类中有大量重复的“流程性”代码。
-
诊断: 代码重复,流程僵化。
-
药方: 模板方法模式。
-
-
代码坏味道: 一个类的构造函数参数列表长得离谱(
new User(a,b,c,d,e,f...))。-
诊断: 对象创建逻辑太复杂。
-
药方: 建造者模式 (Builder Pattern)。
-
推荐书籍
- 《Head First 设计模式》 (Head First Design Patterns) - Eric Freeman & Elisabeth Robson
-
核心内容与思想:
-
它不是在“教模式”,而是在“教原则”。它把“开闭原则”、“依赖倒置”等SOLID原则作为主线,贯穿全书。
-
它的核心是“封装变化”。全书都在教你一件事:找到系统中“会变化”的部分,把它“封装”起来,让它不影响“不变”的部分。
-
(小葵的登录案例,就是“封装”了“登录方式”这个“变化点”)
-
- 《重构:改善既有代码的设计》 (Refactoring: Improving the Design of Existing Code) - Martin Fowler
-
核心内容与思想:
-
代码坏味道 (Code Smells): 本书的精华。它系统性地总结了几十种“代码变坏”的“信号”,比如“过长的方法”(Long Method,张三的
loginV2)、“过大的类”、“重复代码”等。 -
重构手法 (Techniques): 针对每一种“坏味道”,它提供了“处方”——一系列安全、标准化的“重构手法”(如“提炼方法”、“用策略模式替换条件逻辑”)。
-
这本书让你明白,好的设计不是“一次性”设计出来的,而是“持续重构”出来的。
-
结语
小葵的上海“流浪”之旅开始了。她通过“策略模式”,成功地为“用户中心”打下了坚实的地基。她不再是一个“功能实现者”,而是一个“系统设计者”。
而张三,依然在北京的“凤凰”单体上,用if-else奋战在需求一线。他没有被淘汰,但他正在被“固化”。他离“资深”的距离,不是技术的距离,而是“设计思维”的距离。






