在软件行业里,我们习惯了“快速交付”“敏捷迭代”“先上线再说”,却很少停下来问一个基础问题:这个系统的设计到底成不成立。项目上线那一刻不是设计终局,而是设计结果的第一次真正检验。可现实中的大多数系统,在需求变更、团队扩容、性能问题接踵而至时,会迅速暴露出结构上的脆弱。这个时候再回头看,很多人会发现,我们不是没有做设计,而是已经忘记了设计本来的样子。
这篇博客围绕“设计能力衰退”这个问题展开,尝试回答三个问题:什么是真正意义上的软件设计,为什么它在工程实践中不断被稀释,以及如何把设计重新变成团队和个人可以执行的能力。文章会结合代码示例、架构取舍、评审流程和排查清单,尽量让“设计”这件事从口号变成可落地的工程动作。
1. 当我们谈论“设计”时,到底在谈论什么
1.1 “设计”在软件开发中不是一个单一的词
很多人听到“设计”,第一反应是 UI 设计、交互设计、页面视觉。但在软件开发语境中,设计的范围远不止界面层。一个系统的设计至少包含五个层级:
- 需求设计:把模糊的业务诉求转化为明确的功能定义和数据约束。
- 领域设计:识别业务中的核心概念、关系、规则和边界。
- 架构设计:确定系统由哪些模块组成,模块之间如何通信,数据如何流转。
- 代码设计:在类、函数、接口层面保证可读、可测、可扩展。
- 运维与演进设计:考虑部署、监控、灰度、回滚、重构路径。
这五个层级互相影响。领域边界划分错误,会导致架构层被迫做大量妥协;代码层缺乏抽象,会让架构意图无法落地。很多时候我们说的“设计感差”,并不是某一张图不好看,而是这些层级中的某一个或多个出现了结构性缺口。
1.2 为什么说“忘记了设计”是一个工程问题
“忘记了设计”不是指团队里没有人画架构图,也不是指没有设计文档。更深层的问题在于,设计被当成了开发的前置仪式,而不是贯穿交付全过程的持续判断。
典型的失真表现有:
- 设计文档写完就归档,和最终代码完全对不上。
- 需求评审只讨论“做什么”,不讨论“为什么这么做”。
- 为了追赶版本,把多个职责塞进同一个类、同一个接口、同一个模块。
- 代码评审只看功能是否实现,不看边界是否清晰、依赖方向是否正确。
- 技术方案选型靠“哪个流行”“哪个有教程”,而不是靠约束和成本分析。
当这些现象反复出现,系统会逐渐丧失一个很重要的能力:可预期性。新增需求时,团队无法估计改动量;出现问题后,团队无法快速定位。而这些恰恰是设计本该解决的问题。
1.3 设计与“尽早开工”并不冲突
这里要澄清一个容易走极端的误区。强调设计不等于要求团队花大量时间做重文档、画大图、走冗长流程。真正的设计是在有限信息下做高质量决策。
一个务实的设计流程可能只包含几个步骤:
- 明确问题:要解决什么,边界是什么,验收条件是什么。
- 列出约束:时间、团队规模、既有系统、性能要求、合规要求。
- 对比候选方案:至少两个方案,列出取舍依据。
- 确定关键接口和数据结构:让并行开发成为可能。
- 实施中持续校正:设计是活的,一旦发现不合理,及时调整。
这相比“完全不做设计直接敲代码”多花的时间有限,但少走的弯路通常很可观。
2. 设计能力衰退的五个常见原因
2.1 交付周期压缩导致了“先跑通再重构”的惯性
业务节奏越来越快,技术团队经常处于一种“这周不做完就影响业绩”的状态。在这种压力下,“最快能跑通的方案”往往胜出,长期结构被系统性延后处理。更麻烦的是,“跑通”会带来一种虚假的安全感:功能是好的,用户没投诉,性能没爆炸,干吗要动?
但技术债会在一个安静的时间点集中爆发。通常不是第一次上线时,而是第二次需求变更、第三个人接手、第一次流量高峰。真正的问题在于,团队把“重构”当成了一种默认的兜底策略,却没有为重构预留时间窗口和节奏。正确的方式不是严禁快速实现,而是给快速实现配一个明确的“债务记录”和“偿还计划”。
2.2 组件化、低代码和模板工程削弱了结构判断力
现在的开发工具链越来越成熟,脚手架、组件库、低代码平台让“搭出一个可用系统”变得非常容易。这对于业务交付是好事,但也带来一个副作用:很多开发者在真正理解底层结构之前,就完成了应用搭建。
举例来说,一个团队可能熟练使用微服务框架,却说不清楚服务拆分的依据是什么;可以用 ORM 快速完成 CRUD,却不理解事务边界和索引设计;可以使用消息队列做异步解耦,却不分析消息失败后的幂等和补偿方案。工具解决的是执行效率,设计解决的是判断质量。过度依赖工具而缺乏判断训练,会让人逐渐忘记如何从第一性原理出发设计方案。
2.3 评审机制退化成了“过会”而非“思辨”
不少团队有设计评审会,但实际效果不理想。常见的问题有:
- 评审材料太粗,只有一页 PPT 或者一张架构图。
- 评审会上没人提问,或者提问只停留在“这个模块叫什么名字”。
- 评审结论模糊,没有明确给出“同意/不同意/需要修改后复审”。
- 评审只覆盖架构,不覆盖领域模型、接口契约、数据模型和异常场景。
设计评审本应是质量阀门,却变成了流程表演。这会导致团队失去一个重要机制:在动手前暴露结构问题,而不是在代码写完后发现推倒重来成本太高。
2.4 对“抽象”和“模式”的理解流于表面
还有一类团队并非不设计,而是过度设计。他们把设计理解为套用设计模式、引入中间件、拆微服务。结果是抽象层次过多,概念名词满天飞,但每个抽象都缺乏清晰定义。
真正的抽象是为了隐藏变化点、降低成本,不是为了追求代码简短或者显得高级。一个糟糕的抽象,比没有抽象更危险。因为它让代码变得难以追踪,让新成员无法判断该在哪个层改动。要避免这种问题,需要持续追问一个朴素的问题:这个抽象让改动变简单了吗?还是只让入口变简单了?
2.5 缺少对设计结果的回望和复盘
最后一点,也是团队层面最容易忽略的:设计决策很少被复盘。三年前的架构选型为什么是它?当时考虑了哪些方案?哪条假设后来失效了?如果现在重新设计,会做什么不同的事?
没有复盘,设计经验就无法沉淀。同样的错误可能在多个项目里重复发生,而团队却不自知。要恢复设计能力,本质上要恢复的是一种学习闭环:决策、执行、观察结果、修正判断。
3. 用购物车模块的设计演进,重新看“设计基本功”
3.1 三个版本的设计,对应三种思考层次
为了把设计基本功讲具体,这里用一个很常见的案例:购物车模块。先看一个最朴素的设计,很多早期项目都会写出这样的结构。
public class CartService { public void addItem(Long userId, Long productId, Integer quantity) { // 检查用户是否存在 // 检查商品是否存在 // 检查库存 // 保存购物车记录 } public void removeItem(Long userId, Long itemId) { // 删除记录 } public BigDecimal getTotalPrice(Long userId) { // 遍历购物车项 // 查询商品价格 // 求和 } }这个版本不能说错,它能完成基本功能。但问题在于,CartService几乎承担了所有逻辑:校验、查询、计算、持久化。当需求变成“购物车项参与满减”“购物车商品支持预售价”“购物车按店铺分组”时,这个类会迅速膨胀,所有方法开始互相调用,测试也越来越难写。
3.2 进入领域设计:先定义概念边界
设计的关键动作不是写代码,而是先厘清业务中的核心概念。
购物车领域至少存在这样几个概念:
- 购物车项:代表“某个用户加了某个商品的数量”。
- 商品快照:加入购物车时的价格、名称、规格信息。
- 价格计算规则:如原价、促销价、满减、优惠券。
- 购物车校验:库存校验、上下架校验、区域配送校验。
如果把“购物车”当作一个有过期价格和业务规则的领域对象,而不是简单的数据库表,设计会走向完全不同的方向。
public class CartItem { private String itemId; private String skuId; private int quantity; private Money unitPrice; private boolean checked; public Money calcSubtotal() { return unitPrice.multiply(quantity); } }3.3 设计“价格计算”时,把变化点隔离出来
购物车最复杂的地方通常是价格计算。如果直接把价格计算逻辑写在CartService里,每次促销规则变化都要修改核心类。一个更合理的做法是引入策略结构,让规则可以独立扩展。
public interface PriceRule { boolean support(CartContext context); PriceResult apply(CartContext context); }public class FullReductionRule implements PriceRule { @Override public boolean support(CartContext context) { return context.getTotalAmount().greaterThanOrEqual(new Money(300)); } @Override public PriceResult apply(CartContext context) { return PriceResult.discount(new Money(50)); } }这样设计的好处是:新促销规则只需要新增一个实现类,不需要改动CartService。规则之间的组合顺序可以在外部配置,规则是否生效可以通过单测独立验证。
3.4 三个版本的技术启示
| 设计层次 | 核心问题 | 购物车案例 |
|---|---|---|
| 过程式设计 | 功能放在一个方法里 | 校验、查询、计算、持久化全部堆在 CartService |
| 对象式设计 | 业务概念封装为对象 | CartItem 拥有自己的行为和规则 |
| 策略式设计 | 变化点通过接口隔离 | PriceRule 支持规则扩展,不修改核心流程 |
这个演进过程能帮助理解一个关键判断:设计不是一开始就要做最复杂的那套,而是要在需求变化点出现时,判断哪些部分存在稳定边界,然后把稳定边界做成接口,把变化点做成实现。
4. 系统设计中的取舍:结构不是越复杂越好
4.1 设计本质上是“约束下的决策”
很多架构争论无法收敛,不是因为技术方案不够多,而是因为缺少统一的取舍框架。设计决策至少要回答几个问题:
- 这个模块最重要的质量属性是什么?是可用性、一致性、吞吐量,还是开发效率?
- 这个模块的生命周期预计有多长?是短期活动页面,还是核心交易链路?
- 团队熟悉哪些技术栈?迁移成本是否可接受?
- 如果方案失败,回滚路径是什么?
脱离这些约束谈架构是无效的。任何设计方案都需要先声明自己优化的目标,不然讨论最终会变成“谁的偏好更有说服力”。
4.2 一个真实的取舍案例:订单查询是读数据库还是走缓存
以一个订单查询接口为例。初期数据量小,直接查数据库是最简单可靠的方式。随着订单量增长,查询变慢,团队开始考虑缓存方案。
方案 A:只缓存热点订单数据。
优点:实现简单,命中率高,缓存数据一致性风险低。 缺点:冷门订单仍然走数据库,数据库压力下降有限。
方案 B:所有订单数据双写数据库和缓存。
优点:查询延迟稳定,数据库压力显著下降。 缺点:双写一致性问题复杂,一旦缓存和数据库数据不一致,容易引发资损风险。需要额外引入版本号、重试、对账等机制。
最终如何选择,不取决于哪个方案更“先进”,而取决于业务的成本承受能力和一致性要求。订单类数据涉及金额、状态、售后判断,一致性要求极高,缓存方案必须额外设计兜底和补偿。而如果是商品详情页,允许几秒的短暂不一致,缓存策略就可以更激进。
4.3 好设计文档应该记录“放弃过什么”
一份好的设计文档不应该只描述“要怎么做”,还要描述“在什么条件下,我们放弃了什么”。这种信息对后来接手的人特别有价值。
一份实用的技术方案建议包含以下内容:
- 背景与问题定义。
- 目标和约束。
- 候选方案列表。
- 各方案的利弊分析。
- 选定方案及关键接口、数据模型。
- 被放弃方案和放弃理由。
- 风险项和应对策略。
- 上线后的验证指标。
尤其第 6 条和第 8 条,最容易被忽略,也因此最容易造成团队重复踩同一个坑。
5. 让设计重新成为工作流的一部分:轻量级评审机制
5.1 评审不是“审人”,而是“审风险”
设计评审最常见的失败原因是气氛不对。提案人觉得被挑战,评审人觉得走过场。团队应该建立一种共识:设计评审不是审判,而是用集体的视角帮助发现单点思考容易遗漏的风险。
评审的重点应该放在:
- 需求理解是否存在偏差。
- 领域模型是否覆盖了核心业务规则。
- 接口边界是否清晰,是否会造成循环依赖。
- 数据模型是否满足查询和写入的扩展性。
- 异常分支是否都有兜底策略。
- 是否有可回滚的发布方案。
5.2 可以落地的轻量评审流程
不需要重型流程,一个 30 分钟到 45 分钟的设计评审会就足够。建议按以下节奏进行:
- 提案人用 10 分钟讲清楚背景、目标和方案。
- 与会者用 5 分钟默读设计文档或图表。
- 所有人按“风险清单”逐项提问,每个问题只描述风险,不立即讨论解法。
- 提案人记录问题,会后统一处理,不要求现场讲解答案。
- 主持人给出明确结论:通过、有条件通过、需要重新评审。
这种流程的价值在于:不是寻找完美方案,而是把可预见的风险提前暴露出来。
5.3 评审检查表:一张可以反复使用的卡片
| 评审维度 | 检查问题 |
|---|---|
| 需求边界 | 需求方确认过验收条件吗?范围是否明确? |
| 领域建模 | 核心概念是否都有准确命名和定义? |
| 接口设计 | 接口是否幂等?参数是否完整?版本兼容策略是什么? |
| 数据模型 | 唯一索引是否合理?未来扩展字段是否预留? |
| 异常处理 | 超时、重试、熔断、降级分别覆盖哪些场景? |
| 安全 | 权限、越权、敏感数据、限流是否考虑? |
| 可运维性 | 日志是否有关键链路标记?监控指标是否明确? |
| 回滚能力 | 中间件、数据库变更是否可回滚? |
这张表可以随着团队经验不断扩充。它解决的问题是:设计评审不再依赖个别资深专家的直觉,而是让团队在同一个框架下发现系统性风险。
6. 恢复设计能力的实践路径:从个人到团队
6.1 个人层面:刻意练习的四个动作
设计能力不是天赋,而是一种可以被刻意训练的判断力。建议开发者从四个方向持续练习。
第一个动作:阅读优秀源码时,不看实现先猜设计。选择一段感兴趣的框架代码,先通过接口定义和模块边界猜测它的设计意图,再对照源码验证。
第二个动作:接到需求时,先写测试用例再写实现。测试用例实际上是在描述期望行为和边界条件,这个过程会强迫你思考接口设计的合理性。
第三个动作:重构前先画依赖图。任何一个超过 500 行的类,都值得先画一张当前的依赖关系图,再判断哪些依赖可以切断。
第四个动作:写问题复盘。每遇到一次“这里为什么这么难改”,都记录下根因,尝试追溯到是哪个设计决策导致了当前困境。
6.2 团队层面:建立三个机制
个人能力只能解决局部问题,要让设计能力成为团队资产,还需要三个机制。
第一个机制是设计轮值评审。不固定由架构师做评审,而是让每个成员轮流承担评审组织者角色,提前整理风险清单,训练全局视角。
第二个机制是技术雷达更新。团队每两个月列出一次“我们正在使用的技术、我们正在试用的技术、我们已经放弃的技术”,让技术选型保持显性化,而不是靠每个人各自的记忆。
第三个机制是“设计决策日志”。在项目仓库中保留一个文件,专门记录关键设计决策、当时的备选方案和最终选择理由。这部分内容能极大降低新成员的接手成本。
6.3 学习环境与生产环境的差异
对于一个学习型项目,设计可以相对轻量:重点是理解概念和跑通功能,不需要引入完整的监控体系、灰度发布、多环境治理。但在生产环境中,设计的缺失会在故障时被放大。
生产环境设计至少还要关注:
- 配置是否外置化,能否不重新发版就调整参数。
- 日志是否包含 traceId 链路追踪,能否串起一次完整请求。
- 依赖的中间件是否有降级和熔断预案。
- 数据库变更是否有一致的发布回滚方案。
- 是否具备容量评估和压测手段。
学习环境帮助你建立设计的感觉,生产环境才是设计的最终考场。
7. 设计溃败的早期信号与排查思路
7.1 这些信号出现时,设计大概率出了问题
系统不会突然崩溃,结构问题通常会先通过一些“微妙”的信号暴露出来。
| 信号 | 表象 | 可能的根因 | 建议动作 |
|---|---|---|---|
| 需求变更成本突然升高 | 一个简单字段改动涉及多个服务 | 领域边界切分错误 | 重新梳理领域依赖,明确数据归属 |
| 改 A 模块导致 B 模块故障 | 模块间出现隐式共享状态或全局变量 | 依赖边界不清晰 | 检查依赖方向,切断隐式关联 |
| 同一个逻辑在多处重复实现 | 促销、价格计算散落多处 | 缺少公共抽象 | 抽取稳定接口,收敛实现 |
| 单元测试难写 | 被测类依赖大量 Mock | 类职责过多或依赖过深 | 拆分职责,接口注入依赖 |
| 上线后频繁回滚 | 变更影响面无法评估 | 缺少版本兼容设计 | 制定接口演进规则,增加灰度验证 |
| 代码评审争论聚焦命名 | 团队不再讨论结构只讨论风格 | 缺乏设计共识 | 建立评审风险清单 |
7.2 从现象倒推根因的排查链路
遇到设计引发的系统性故障,排查顺序非常重要。建议按以下链路推进:
- 先复现现象,确认触发条件,不要直接改代码。
- 再梳理变更历史:最近哪些模块、哪些配置、哪些数据结构发生过变化。
- 再检查调用链路:确认异常是入口参数问题、中间逻辑问题还是依赖服务问题。
- 然后检查数据模型:是否存在隐式状态、数据归属不清、共享表结构。
- 最后检查抽象边界:是否出现了“本不该知道别人内部细节”的跨层调用。
这套排查思路的核心是:不把设计问题当成一个“改一行就能解决”的局部 bug。当现象反复出现、修了又犯时,应该向上追溯,看是不是设计边界本身不成立。
7.3 预防:让设计问题在代码评审前暴露
排错之后要做的是把问题挡在早期。推荐把设计检查前移到需求评审阶段。需求评审时,至少回答四个问题:
- 这个需求属于哪个领域,领域边界是否需要调整?
- 新增的字段和状态,现有数据模型是否需要变更?
- 有哪些外部系统依赖,接口契约是否达成一致?
- 如果需求是临时的,它和核心模型的耦合是否能隔离?
需求阶段解决了这些问题,代码评审阶段的设计争议会大幅降低。
8. 一份可以带走的设计检查清单
最后把这些内容浓缩成一份可以在动手前和交付前反复使用的清单。它不追求覆盖所有场景,只列出最有普适性的检查项。
8.1 动手前检查
- 能否用一段话讲清楚这次要解决的问题?
- 能否列出至少两个候选技术方案,并说明各自取舍?
- 是否明确了数据的归属方和读写边界?
- 接口命名是否体现了业务语义,而不是技术实现细节?
- 是否识别出了可变部分和稳定部分?
8.2 编码中检查
- 类是否只有一个职责?
- 方法是否控制在可理解的复杂度内?
- 依赖方向是从稳定的核心指向易变的外部实现吗?
- 是否有隐藏的全局状态或隐式共享?
- 异常分支是否有明确的失败策略,而不是默默吞掉?
8.3 交付前检查
- 是否存在重复逻辑可以收敛实现?
- 是否有足够的日志帮助我们理解线上行为?
- 数据库变更是否能回滚?
- 发布顺序是否考虑了依赖之间的一致性问题?
- 是否有监控指标来验证设计是否达到预期?
8.4 定期复查
- 最近三次需求变更中,哪些改动比预期困难,根因是什么?
- 架构图中描述的模块边界,和真实代码中的依赖关系是否一致?
- 团队是否在无意识中形成了大量“特殊处理”分支?
- 是否存在若干领域专家才能维护的“黑盒模块”?
- 如果把当前系统中最复杂的模块重写一遍,团队会在哪些设计决策上做不同选择?
这份清单不需要一次性全部完成,但每次设计和评审时,挑选其中几项重点检查,长期来看会显著改善系统结构。
“Have We Forgotten How to Design?” 这个问题没有标准答案,但每个团队都可以通过自己的实践回答。设计能力的衰退不是一个不可逆的过程,它可以通过刻意训练、评审机制、决策日志和复盘习惯逐步恢复。核心判断只有一条:设计不是开发流程里的一个阶段,而是从需求到交付、从个人到团队的持续判断能力。把这个问题重新纳入日常思考,才是找回设计感的第一步。