「设计模式与范式」系列 Day06
写在前面
学完一堆设计模式之后,最容易踩的坑不是"不会用",而是"用过头"------看哪段代码都想套个模式上去,结果代码没变好读,反而多了几层莫名其妙的抽象。另一个极端是完全不设计,几十行的 if-else 一路堆到底。这篇不讲某个具体模式,讲怎么在这两个极端之间找到那个平衡点。
一、是什么:设计的初心,以及两种偏离
应用设计模式的初心只有一个:提高代码的可读性、可扩展性、可维护性。所有具体的设计动作都应该服务于这个初心,偏离了它,不管代码看起来多"高级",都算不上好设计。
设计原则和思想是心法,设计模式只是招式------掌握了心法,才能判断什么时候该用哪招、甚至不套用任何一种现成模式也能设计出合理的结构;只背招式不懂心法,遇到真实场景很容易生搬硬套。围绕这个初心,会出现两种相反方向的偏离:
- 过度设计:为了用模式而用模式,代码没有实际的可扩展性痛点,却提前抽象出一堆用不上的接口和类。
- 设计不足:明明代码已经出现可读性、可扩展性问题,却图省事继续堆砌,缺乏应有的封装和拆分。
判断某处设计是不是"过头"了,有个简单的自问标准:这样设计到底提高了代码质量的哪个方面,能不能讲清楚。如果理由含糊、牵强,甚至要事后现凑几个"满足开闭原则"这种不痛不痒的说法来搪塞,基本可以断定是过度设计。
二、为什么:两种偏离是怎么发生的
过度设计 通常源于"先有方案、后找理由":看到一个场景觉得眼熟------好像在哪本书里见过用某个模式解决过类似问题------不管合不合适先套上去,等真被问起来,再临时找几个笼统的理由自圆其说。这种做法本末倒置了设计该有的顺序,正确的顺序应该是先分析代码存在的具体痛点(可读性差、改一处影响一片等),再针对痛点挑合适的模式,而不是照葫芦画瓢。
设计不足则常常缺三样东西:一是理论储备不够,不熟悉设计原则、模式、编码规范,手里没有可用的工具;二是缺少把理论和实践结合的刻意训练,知识学过就忘,遇到具体问题联想不到对应的知识点;三是缺乏代码质量意识,写代码时压根没去想"这部分未来会不会变、现在这样写会不会让以后加功能很困难"。
往更底层想,设计模式要解决的本质问题是用更好的代码结构把一大坨代码拆开、解耦 ------创建型模式解耦"创建对象"和"使用对象",结构型模式解耦不同的功能模块,行为型模式解耦不同的行为逻辑,解耦的目的都是应对代码复杂度。这也解释了两种偏离为什么会发生在同一条线的两端:复杂度不够却上了重设计,是过度设计;复杂度已经存在却不设计,是设计不足,两者本质上是同一把尺子量出来的两个反方向的偏差,不是互不相关的两个问题。
三、怎么用:一个订单折扣需求里的平衡点
用一个几乎所有电商系统都会遇到的需求来看这个平衡点具体怎么找:订单满减。
只有一条规则时,简单判断就够
java
public BigDecimal calcDiscount(BigDecimal amount) {
if (amount.compareTo(new BigDecimal("100")) >= 0) {
return amount.subtract(new BigDecimal("20"));
}
return amount;
}
这时候引入策略模式、工厂类反而是过度设计的典型:
java
// 只有一种规则,却提前抽象成这样------多了两个类,却没有换来任何实际收益
public interface DiscountStrategy {
BigDecimal apply(BigDecimal amount);
}
public class FullReductionStrategy implements DiscountStrategy { /* ... */ }
public class DiscountStrategyFactory { /* 只有一个实现,工厂形同虚设 */ }
代码量涨了,可读性反而下降------看这段代码的人得先跳到接口、再跳到工厂、再跳到唯一的实现类,才能确认"这就是满 100 减 20"这么一件事。
真正出现痛点时,才是重构的时机
后来产品提了新需求:满减之外还要叠加会员折扣、优惠券------规则会持续增加、还会调整组合方式。这才是具体、真实的痛点,此时重构成策略模式(或者责任链)才是"先有问题后有方案":
java
public interface DiscountRule {
BigDecimal apply(BigDecimal amount);
}
public class FullReductionRule implements DiscountRule { /* 满100减20 */ }
public class MemberDiscountRule implements DiscountRule { /* 再打9折 */ }
public class CouponRule implements DiscountRule { /* 再减固定金额 */ }
public class DiscountChain {
private final List<DiscountRule> rules;
public BigDecimal calc(BigDecimal amount) {
for (DiscountRule rule : rules) {
amount = rule.apply(amount);
}
return amount;
}
}
同样一个策略模式,第一版是过度设计,第二版是恰到好处------区别不在模式本身,在于复杂度是不是真实存在。
常见的坑:怎么判断"现在要不要就上"
拿不准要不要现在引入某个设计时,可以换个角度想:如果暂时不用,等哪天真需要它的时候再重构,改动量大不大 ?如果不大,那就先不用,怎么简单怎么来。对于十万行以内、团队稳定、大家都熟悉业务的项目,即便把相关代码整个推倒重写,也花不了太久,完全不必为遥远的、还没发生的扩展性提前买单------这也是持续重构能有效遏制过度设计的原因:先用最简单的方案满足当下的真实需求,等痛点真正出现再重构,而不是一开始就为"可能用得上"的未来设计。
不同项目场景,该投入的设计精力也不一样
设计好坏没法脱离具体场景空谈,同一个设计放到不同项目里,合理程度完全不同:
| 项目场景 | 该花多少精力在设计上 |
|---|---|
| 抢时间上市的项目(比如手游首发) | 尽量少,市场验证不通过很快就会废弃,代码质量优先级让位于上市速度 |
| 长周期重投入项目(比如 MMORPG 大型端游) | 多,开发周期以年计、推倒重来成本极高,前期设计不到位后期基本无法挽回 |
| 偏底层的框架/通用组件代码 | 多,影响面广,一处设计缺陷会被所有调用方放大 |
| 普通业务系统 | 适中,代码量不大、跟其他项目耦合少,出问题影响范围有限 |
四、面试追问
Q1:应用设计模式的心法和招式分别指什么?
设计原则和思想是心法,设计模式只是具体的招式。应用任何设计模式的初衷都是为了提高代码的可读性、可扩展性、可维护性,而不是模式本身。判断是不是过度设计的简单标准,是问自己这样设计到底提高了代码质量的哪个方面,讲不清楚或者理由牵强,基本就是过度设计。
Q2:怎么区分一段设计是先有问题后有方案,还是先有方案后找理由?
看动机:是不是因为代码已经出现了具体的可读性或可扩展性痛点,才去挑一个合适的设计模式来解决;如果只是看到某个场景跟书上的例子相似就照套模式,事后再找几个不痛不痒的理由(比如满足了开闭原则)搪塞,就是先有方案后找理由,是过度设计的典型信号。
Q3:为什么说持续重构是避免过度设计的有效手段?
很多复杂设计是为了应对"未来可能出现"的需求预判,一旦预判错了,团队就要一直背负这个复杂设计前行,还很难删掉。持续重构让团队可以先用最简单的方案应对当下的真实需求,等真正出现扩展痛点时再重构成对应的设计模式,避免为不一定发生的未来买单。
Q4:避免设计不足需要具备哪三个条件?
一是理论知识储备,熟练掌握设计原则、思想、编码规范和设计模式;二是刻意训练,把理论和实际场景结合反复实践,否则学过的知识点遇到问题时想不起来用;三是代码质量意识,写代码前主动去想哪些地方会变、哪些不会变,这样写会不会让以后加功能变困难。
Q5:为什么说脱离具体场景谈设计是不合理的?
设计好坏本身是主观判断,脱离场景空谈没有意义,必须放到具体项目的约束里评判。比如抢时间上市的手游项目,代码质量可以先放一放;投入巨大、以年计开发周期的大型端游,推倒重来成本极高,就必须前期多花时间设计;偏底层的框架和通用组件,设计缺陷会被所有调用方放大,也值得多投入;普通业务系统影响面有限,可以适度放低要求。
下一篇预告
Day07 三大编程范式串讲:面向对象、面向过程与函数式编程的边界在哪。