ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

框架越强越要会设计:用策略模式重构订单模块,找回代码设计感

框架越强越要会设计:用策略模式重构订单模块,找回代码设计感 1. 我们真的还记得“设计”吗最近在排查一个历史订单模块时我盯着满屏的 if-else 和互相交叉的 Service 方法突然意识到一个有些扎心的问题我们是不是已经忘了怎么设计软件这个疑问不是凭空出现的。那是一个已经迭代了三年的项目需求不断叠加代码越写越快但每次改动都会牵动一片逻辑。调用方不知道应该调哪个方法新同学看不懂一个状态从下单到出库到底经历了哪些流转。整个模块不是没有代码而是缺少让代码稳定运转的骨架。“设计”这个词在软件开发里过去是架构师讨论的专利而现在变成了很多开发者跳过的一个步骤。我们接了需求就建表建完表就写接口写通接口就提测。功能确实上了但软件本身却没有被“设计”过。这里的“设计”指的并不是交互稿上的按钮位置而是指软件的系统设计模块怎么划分、职责怎么分配、数据怎么流转、边界怎么界定、变化点怎么隔离。这些决策才是软件长期可维护的关键。本文想和你系统聊一聊在框架能力越来越强、开发效率越来越高的今天为什么我们反而容易丢失设计能力一个模块从“能跑”到“设计良好”到底差在哪以及在日常项目中我们怎样才能把“设计”这件事重新捡起来。内容适合正在做后端开发、写过大量业务代码但觉得维护越来越吃力的开发者阅读也适合技术 Leader 用来做代码评审和团队培养的参考。需要提前说明的是这是一篇偏工程实践与思维方法论的文章不绑定任何特定框架的版本号。代码示例以常见的 Java Spring 项目风格呈现核心目的是把设计思路讲清楚不同技术栈的读者可以迁移到自己的项目中理解。1.1 从一道重构题说起假设产品提了一个很常见的需求用户下单后系统要根据不同的商品类型计算运费。普通商品按重量计算生鲜商品按冷链配送距离计算数码类商品满额包邮。很多开发者的第一反应是写一个方法public BigDecimal calculateFreight(Order order) { BigDecimal freight; if (NORMAL.equals(order.getType())) { freight order.getWeight().multiply(new BigDecimal(10)); } else if (FRESH.equals(order.getType())) { freight order.getDistance().multiply(new BigDecimal(2)); } else if (DIGITAL.equals(order.getType())) { freight order.getAmount().compareTo(new BigDecimal(499)) 0 ? BigDecimal.ZERO : new BigDecimal(20); } else { throw new IllegalArgumentException(未知商品类型); } return freight; }这段代码功能上没有任何问题跑起来也很快。但如果你做过半年以上的维护工作看到这种写法可能已经开始皱眉了一旦增加新的商品类型就要回来改这个 if-else一旦判断条件变复杂这个方法会越来越长一旦出现并行需求多个分支同时修改会引发冲突。这就是“没有设计”的典型表现代码完成了功能但没有为未来的变化预留结构。我们后面会基于这个场景做完整重构这里先留一个印象。1.2 设计不等于画架构图很多团队会把“设计”理解为画一张漂亮的架构图或者写一份没人看的概要设计文档。这是一种误解。画图只是设计的表达形式真正的设计发生在你决定“哪段逻辑应该放在哪一层”“哪个类应该对谁负责”“哪个接口应该暴露哪些能力”的时刻。设计是一系列决策的组合订单状态的流转应该由谁控制运费计算规则变化时哪些代码必须被修改外部调用方依赖的是抽象接口还是具体实现一张大表要不要拆分拆了之后事务怎么处理异常是抛出去让上层处理还是内部吞掉并降级这些决策可能在写代码前完成也可能在代码评审中被推翻重来但不会因为“用了 Spring Boot”“用了微服务”就自动消失。换句话说框架解决了连接问题但没有解决结构问题。Spring 能让你的 Bean 自动装配但不会告诉你订单服务和库存服务应不应该拆成两个模块。MyBatis 能帮你完成对象映射但不会告诉你订单明细表应该怎么设计索引。这些都需要靠人的设计能力来完成。1.3 忘记设计的直观信号怎么判断一个项目已经处于“忘记设计”的状态可以从代码里找信号Service 层越来越胖。所有业务逻辑都堆在 Service 方法里一个方法七八十行起步事务注解满天飞。到处可见“万能 DTO”。同一个对象在 Controller、Service、Mapper 三层之间来回传递字段越加越多职责越来越模糊。修改代码像排雷。改一个看似无关的校验逻辑结果影响了另一个接口的行为回归测试范围越估越大。单元测试难写。核心逻辑全部依赖 Spring 容器和数据库离开环境就没办法测试。命名失去语义。方法名叫handleData、类名叫OrderUtil没有人能从名字看出它的职责边界。出现这些信号问题通常不在某个人的编码水平而在整个团队默认“设计阶段可以省略直接堆代码就行”。这种默认短期看起来效率很高长期则会把技术债累积到难以偿还的程度。2. 环境准备与示例项目说明为了让后面的内容能落在具体的代码上我们不妨约定一个最小的技术背景。这里不追求引入复杂的微服务架构而是用一个最典型的业务模块作为载体。2.1 示例技术栈本文示例以 Java 后端项目为背景重点不在某个具体版本的 API 上。如果你使用的是 Spring Boot 2.x、Spring Boot 3.x 或其他语言的 Web 框架同样能看懂设计思路。建议的本地环境JDK17 或 21如果团队还在用 JDK 8也不影响阅读核心 API 不涉及新版本特性。Spring Boot2.7 或 3.x本文示例只用到 MVC 和依赖注入能力。数据库MySQL 8.x用于演示订单表和明细表的基本结构。ORMMyBatis-Plus 或 Spring Data JPA 均可代码中会弱化 ORM 细节。构建工具Maven 或 Gradle。版本不需要完全对齐重点是理解设计思路。真实项目中请以你们的统一技术栈为准不要照搬示例中的任何版本号。2.2 示例项目结构我们将围绕一个订单模块展开模拟下单、计算运费、计算优惠、生成订单这几个业务动作。示例项目会从“只求能跑的第一版”重构到“具备良好结构的第二版”。order-service ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/order │ │ │ ├── OrderApplication.java │ │ │ ├── controller │ │ │ │ └── OrderController.java │ │ │ ├── service │ │ │ │ ├── OrderService.java │ │ │ │ └── impl │ │ │ │ └── OrderServiceImpl.java │ │ │ ├── model │ │ │ │ ├── entity │ │ │ │ │ └── OrderEntity.java │ │ │ │ ├── dto │ │ │ │ │ └── CreateOrderRequest.java │ │ │ │ └── enum │ │ │ │ └── OrderType.java │ │ │ ├── repository │ │ │ │ └── OrderRepository.java │ │ │ └── strategy │ │ │ ├── FreightStrategy.java │ │ │ └── impl │ │ │ ├── NormalFreightStrategy.java │ │ │ ├── FreshFreightStrategy.java │ │ │ └── DigitalFreightStrategy.java │ │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com/example/order │ └── OrderFreightTest.java这个结构是“设计”的第一步每个代码文件都有明确的归属层。controller 负责接收请求与响应service 负责业务编排strategy 负责具体规则实现repository 负责数据访问。后续新增一种运费规则时只需要新增一个 strategy 实现类不需要改动 controller 和 service。3. 核心设计拆解怎样才算“会设计”在设计这件事上有一个常用的判断标准你的代码能否把“稳定的部分”和“变化的部分”分离开。稳定的部分比如订单的基本流程、数据写入的仓库接口应该保持稳定变化的部分比如运费规则、优惠策略、通知渠道应该被抽象出来方便扩展。围绕这个标准我们可以拆解出几个设计维度。3.1 先定义边界需求不是功能清单很多同学拿到需求后第一反应是“我要写哪些接口”“我要建哪些表”。这个顺序其实反了。正确的第一步是划分业务边界。以订单为例。订单模块的核心边界是什么它接收用户的购买意图校验库存与价格生成订单记录之后驱动支付、发货、收货等后续流程。运费计算、优惠计算、库存扣减这些是订单模块内部的分支能力但不应该散落在订单服务之外。边界定义清楚了你才知道什么逻辑必须放在模块内部什么逻辑应该通过接口交给外部。比如库存扣减订单模块不应该直接操作库存表而应该调用库存服务提供的扣减接口。这不是技术层面的限制而是为了让每个模块保持独立的演进能力。3.2 识别变化点设计是为了容纳变化设计能力高低的差别往往体现在对变化点的预判上。运费规则一定会变这是业务常识。所以运费计算就不应该是一个硬编码的 if-else而应该是一组可以灵活替换的策略对象。优惠活动也一定会变今天满减、明天折扣、后天返券所以优惠计算也应该与主流程解耦。但变化点不能靠猜。常见的做法是问自己三个问题这个规则在业务上是否可能独立变化这个规则和主流程是强耦合还是弱耦合如果我要换一套实现需要改动多少个文件回答完这三个问题你基本就能判断哪段逻辑值得设计成抽象接口。注意不要过度设计如果某个规则在可预见的周期内不会变化那么把它写成一个私有方法并加上清楚的注释也是合理的选择。设计的目标是让代码在“当前需求”和“合理变化的未来”之间找到平衡而不是为了抽象而抽象。3.3 接口先行从调用方视角思考设计接口时最容易犯的错误是“实现驱动”——先想用哪种数据结构来实现再暴露方法。更好的方法是“调用方驱动”先假设自己是要调用这段逻辑的人想想你希望这个接口长什么样。比如运费计算接口如果从调用方视角看你希望传入什么参数返回什么结果public interface FreightStrategy { boolean supports(String orderType); BigDecimal calculate(CreateOrderRequest request); }这个接口有两个方法supports判断当前策略是否支持这种订单类型calculate执行具体的计算逻辑。调用方可以先用supports找到匹配的策略再调用calculate拿结果。这样后续增加新的商品类型只需要新增一个实现类不需要改动调用方。如果一开始就把方法设计成直接接收所有参数并返回结果比如calculate(String type, BigDecimal weight, BigDecimal distance, BigDecimal amount)那么参数变化时接口签名就得跟着变所有调用方都会受影响。从调用方视角思考就是要减少这种“签名敏感”的设计。3.4 数据建模表结构与领域模型分开思考还有一类常见的设计问题是数据库表设计完全跟着页面表单走表单有什么字段表就建什么字段领域模型没有任何业务含义。好的做法是先思考领域模型。订单是一个聚合根它包含订单号、用户、金额、状态等核心属性订单明细是订单的子实体它包含商品、数量、单项金额。运费、优惠金额可以体现在订单的金额字段中但计算逻辑应该独立于存储结构。表结构设计时至少要遵循几个原则核心字段必须保证数据一致性。例如金额字段使用DECIMAL而不是FLOAT。状态字段建议设计一个枚举类而不是到处写魔法值。频繁查询的字段必须建索引但索引不是越多越好。如果担心历史数据影响新业务提前评估是否需要拆表或加字段版本号。数据模型和领域模型不是一回事。在领域对象里订单是一个完整的概念在数据库里订单可能被拆成订单主表和订单明细表两张表。它们之间的转换关系应当在 repository 层完成而不是让领域层直接感知表结构。4. 完整实战订单模块从“能跑”到“设计良好”理论部分讲了不少接下来我们用具体的代码走一遍完整实战。为了节约篇幅我们聚焦“下单时计算运费”这个场景展示从“第一版”到“设计之后”的差别。4.1 第一版只求能跑的订单服务很多新项目从第一版代码开始就已经种下了维护困难的种子。下面这个OrderServiceImpl包含了下单、校验、计算运费、计算优惠、保存订单的全过程。文件路径src/main/java/com/example/order/service/impl/OrderServiceImpl.javapackage com.example.order.service.impl; import com.example.order.model.entity.OrderEntity; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.repository.OrderRepository; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.UUID; Service public class OrderServiceImpl { private final OrderRepository orderRepository; public OrderServiceImpl(OrderRepository orderRepository) { this.orderRepository orderRepository; } public OrderEntity createOrder(CreateOrderRequest request) { // 1. 校验必填字段 if (request.getUserId() null) { throw new IllegalArgumentException(用户不能为空); } if (request.getItems() null || request.getItems().isEmpty()) { throw new IllegalArgumentException(商品明细不能为空); } // 2. 计算商品总金额 BigDecimal totalAmount BigDecimal.ZERO; for (CreateOrderRequest.Item item : request.getItems()) { totalAmount totalAmount.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 计算运费 BigDecimal freight BigDecimal.ZERO; String type request.getOrderType(); if (NORMAL.equals(type)) { freight request.getTotalWeight().multiply(new BigDecimal(10)); } else if (FRESH.equals(type)) { freight request.getDeliveryDistance().multiply(new BigDecimal(2)); } else if (DIGITAL.equals(type)) { if (totalAmount.compareTo(new BigDecimal(499)) 0) { freight BigDecimal.ZERO; } else { freight new BigDecimal(20); } } // 4. 计算优惠金额假设新用户立减 10 元 BigDecimal discount BigDecimal.ZERO; if (request.getIsNewUser() ! null request.getIsNewUser()) { discount new BigDecimal(10); } // 5. 计算应付金额 BigDecimal payAmount totalAmount.add(freight).subtract(discount); // 6. 构建订单实体并保存 OrderEntity order new OrderEntity(); order.setOrderNo(UUID.randomUUID().toString().replace(-, )); order.setUserId(request.getUserId()); order.setTotalAmount(totalAmount); order.setFreight(freight); order.setDiscount(discount); order.setPayAmount(payAmount); order.setStatus(CREATED); return orderRepository.save(order); } }这段代码在第一次开发时写起来很快。但如果你要在这个基础上加一个新的商品类型、改优惠规则、增加库存扣减每一次修改都要回到这个方法里动逻辑。方法越来越长测试越来越难写回归范围越来越大。4.2 设计评审这版代码为什么难维护我们来做一次评审找出上面代码里的核心问题。第一职责混乱。订单校验、金额计算、运费计算、优惠计算、订单保存全部堆在一个方法里。任何一个环节的规则变化都会导致整个方法被修改。从代码可读性来说这个方法已经超过 60 行超过了大多数团队定义的“方法体长度上限”。第二规则硬编码。运费规则和优惠规则被写死在 if-else 中新增一种规则就得修改这段代码。更麻烦的是如果两个需求并行开发一个要改运费规则一个要改优惠规则两个分支就会在同一个方法上冲突。第三可测试性差。要测试这段逻辑必须构造一个完整的CreateOrderRequest对象然后走完整个方法。无法单独测试某种运费计算是否正确。团队里如果要写单元测试大概率会选择跳过这类方法因为测试成本太高。第四数据一致性风险。假设保存订单时需要扣减库存直接把这个逻辑加在save前后如果扣库存失败订单却已经保存就会产生脏数据。没有清晰的业务边界事务边界也很难界定。4.3 第二版围绕稳定接口重新设计第二版的目标不是写得“更花哨”而是把每个变化的节点独立出来让主流程变得稳定。首先定义运费计算策略接口。文件路径src/main/java/com/example/order/strategy/FreightStrategy.javapackage com.example.order.strategy; import com.example.order.model.dto.CreateOrderRequest; import java.math.BigDecimal; public interface FreightStrategy { boolean supports(String orderType); BigDecimal calculate(CreateOrderRequest request); }接着实现三种商品类型的运费策略。文件路径src/main/java/com/example/order/strategy/impl/NormalFreightStrategy.javapackage com.example.order.strategy.impl; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.strategy.FreightStrategy; import org.springframework.stereotype.Component; import java.math.BigDecimal; Component public class NormalFreightStrategy implements FreightStrategy { Override public boolean supports(String orderType) { return NORMAL.equals(orderType); } Override public BigDecimal calculate(CreateOrderRequest request) { return request.getTotalWeight().multiply(new BigDecimal(10)); } }文件路径src/main/java/com/example/order/strategy/impl/FreshFreightStrategy.javapackage com.example.order.strategy.impl; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.strategy.FreightStrategy; import org.springframework.stereotype.Component; import java.math.BigDecimal; Component public class FreshFreightStrategy implements FreightStrategy { Override public boolean supports(String orderType) { return FRESH.equals(orderType); } Override public BigDecimal calculate(CreateOrderRequest request) { return request.getDeliveryDistance().multiply(new BigDecimal(2)); } }文件路径src/main/java/com/example/order/strategy/impl/DigitalFreightStrategy.javapackage com.example.order.strategy.impl; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.strategy.FreightStrategy; import org.springframework.stereotype.Component; import java.math.BigDecimal; Component public class DigitalFreightStrategy implements FreightStrategy { private static final BigDecimal FREE_SHIPPING_THRESHOLD new BigDecimal(499); private static final BigDecimal DEFAULT_FREIGHT new BigDecimal(20); Override public boolean supports(String orderType) { return DIGITAL.equals(orderType); } Override public BigDecimal calculate(CreateOrderRequest request) { BigDecimal totalAmount request.getItems().stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); if (totalAmount.compareTo(FREE_SHIPPING_THRESHOLD) 0) { return BigDecimal.ZERO; } return DEFAULT_FREIGHT; } }优惠金额同样可以抽象为策略但为了不过度设计这里我们先用一个独立的DiscountService承载。文件路径src/main/java/com/example/order/service/DiscountService.javapackage com.example.order.service; import com.example.order.model.dto.CreateOrderRequest; import org.springframework.stereotype.Service; import java.math.BigDecimal; Service public class DiscountService { public BigDecimal calculateDiscount(CreateOrderRequest request) { if (Boolean.TRUE.equals(request.getIsNewUser())) { return new BigDecimal(10); } return BigDecimal.ZERO; } }订单主服务只需要负责编排不再关心每个规则的细节。文件路径src/main/java/com/example/order/service/impl/OrderServiceImpl.javapackage com.example.order.service.impl; import com.example.order.model.entity.OrderEntity; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.repository.OrderRepository; import com.example.order.service.DiscountService; import com.example.order.strategy.FreightStrategy; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.List; import java.util.UUID; Service public class OrderServiceImpl { private final OrderRepository orderRepository; private final DiscountService discountService; private final ListFreightStrategy freightStrategies; public OrderServiceImpl(OrderRepository orderRepository, DiscountService discountService, ListFreightStrategy freightStrategies) { this.orderRepository orderRepository; this.discountService discountService; this.freightStrategies freightStrategies; } public OrderEntity createOrder(CreateOrderRequest request) { validate(request); BigDecimal totalAmount calculateTotalAmount(request); BigDecimal freight calculateFreight(request); BigDecimal discount discountService.calculateDiscount(request); BigDecimal payAmount totalAmount.add(freight).subtract(discount); OrderEntity order buildOrder(request, totalAmount, freight, discount, payAmount); return orderRepository.save(order); } private void validate(CreateOrderRequest request) { if (request.getUserId() null) { throw new IllegalArgumentException(用户不能为空); } if (request.getItems() null || request.getItems().isEmpty()) { throw new IllegalArgumentException(商品明细不能为空); } } private BigDecimal calculateTotalAmount(CreateOrderRequest request) { return request.getItems().stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } private BigDecimal calculateFreight(CreateOrderRequest request) { return freightStrategies.stream() .filter(strategy - strategy.supports(request.getOrderType())) .findFirst() .orElseThrow(() - new IllegalArgumentException(未知商品类型)) .calculate(request); } private OrderEntity buildOrder(CreateOrderRequest request, BigDecimal totalAmount, BigDecimal freight, BigDecimal discount, BigDecimal payAmount) { OrderEntity order new OrderEntity(); order.setOrderNo(UUID.randomUUID().toString().replace(-, )); order.setUserId(request.getUserId()); order.setTotalAmount(totalAmount); order.setFreight(freight); order.setDiscount(discount); order.setPayAmount(payAmount); order.setStatus(CREATED); return order; } }第二种写法最明显的变化是主流程中的每一步都是一次独立的方法调用阅读代码时不需要关心内部细节。未来如果新增一种“会员免运费”策略只需要再写一个MemberFreightStrategy实现类然后让 Spring 自动注入到这个List中OrderServiceImpl一行代码都不用改。4.4 运行与验证为了验证策略模式的可测试性我们写一个不依赖 Spring 容器的单元测试。文件路径src/test/java/com/example/order/OrderFreightTest.javapackage com.example.order; import com.example.order.model.dto.CreateOrderRequest; import com.example.order.strategy.FreightStrategy; import com.example.order.strategy.impl.DigitalFreightStrategy; import com.example.order.strategy.impl.FreshFreightStrategy; import com.example.order.strategy.impl.NormalFreightStrategy; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; class OrderFreightTest { private final ListFreightStrategy strategies List.of( new NormalFreightStrategy(), new FreshFreightStrategy(), new DigitalFreightStrategy() ); Test void should_calculate_normal_freight() { CreateOrderRequest request new CreateOrderRequest(); request.setOrderType(NORMAL); request.setTotalWeight(new BigDecimal(5)); BigDecimal freight calculate(request); assertEquals(0, new BigDecimal(50).compareTo(freight)); } Test void should_calculate_digital_free_shipping_over_threshold() { CreateOrderRequest request new CreateOrderRequest(); request.setOrderType(DIGITAL); CreateOrderRequest.Item item new CreateOrderRequest.Item(); item.setPrice(new BigDecimal(499)); item.setQuantity(2); request.setItems(List.of(item)); BigDecimal freight calculate(request); assertEquals(0, BigDecimal.ZERO.compareTo(freight)); } private BigDecimal calculate(CreateOrderRequest request) { return strategies.stream() .filter(strategy - strategy.supports(request.getOrderType())) .findFirst() .orElseThrow(() - new IllegalArgumentException(未知商品类型)) .calculate(request); } }这个测试没有启动数据库没有装配 Spring 容器只关注“某种类型的运费计算是否正确”。将来任何一项规则变化都可以通过直接修改对应的测试用例来保护。这就是设计带来的工程红利问题可以被更快地定位回归测试范围可以被更精准地控制。5. 设计质量排查清单很多团队不是不想设计而是不知道从哪个角度开始检查。下面这份清单可以在代码评审时逐项对照也可以用来给存量模块做体检。5.1 代码层面排查检查项关注点风险提示方法长度Service 方法是否超过 50 行过长说明职责过多需要拆分分支复杂度是否出现大量 if-else 判断类型或状态新增类型必须修改旧代码违反开闭原则魔法值是否直接写字符串或数字常量枚举类和常量类可以让语义更清晰依赖方向Controller 是否直接操作 Mapper依赖应逐层向下避免跨层调用事务边界Transactional是否随意加在公有方法上事务范围过大容易引发锁竞争和长事务5.2 架构层面排查检查项关注点风险提示模块边界订单模块是否直接操作库存表跨模块调用应走接口避免数据库耦合扩展方式新增运费规则需要改几个文件理想情况下新增一个策略类即可数据一致性扣库存失败时订单如何处理需要定义事务边界或补偿机制状态管理订单状态流转是否有统一入口状态机比散落的 if 判断更可控外部依赖每次请求是否串行调用多个外部服务考虑并行调用或异步削峰架构层面的排查要结合团队实际的部署形态而定。不要因为“微服务”“中台”这些概念就盲目拆分。在一个单体应用里模块之间保持清晰的包边界和组织边界同样是一种有效的设计。5.3 性能与安全设计设计不只是结构问题还涉及运行时行为和安全性。以下几点在生产环境中尤其重要数据库访问必须最小化。批量操作不要放在 for 循环里逐条执行要利用批量插入和批量更新。金额计算必须用BigDecimal或数据库的定点数类型严禁使用浮点类型。外部接口调用要配置超时和重试策略避免一个慢接口拖垮整个请求链路。对涉及权限的接口必须在设计阶段就明确谁能调用、能操作哪些数据不能在代码写完后才想起鉴权。生产环境数据变更必须经过测试环境验证并保留备份和回滚方案。尤其是在同时涉及订单、金额、库存这类核心数据时任何没有备份的修改都等同于事故。安全设计不应该是一个附加模块而应该是系统设计的一部分。在评审接口时先问“这个接口如果被恶意调用会怎样”比上线后再补防护要省力得多。6. 最佳实践与工程建议6.1 把设计环节放进日常迭代不要指望每次都做一个完整的“总体设计”再开始写代码。大多数业务迭代并不会改变系统架构所以我们可以用一种更轻的方式每个迭代在动手编码前花十几分钟做一次“局部设计”。具体来说就是先写一个简短的设计说明不超过一页纸包括本次需求涉及哪些领域对象现有代码中哪些方法会被修改是否存在新的变化点它是需要抽象还是一个固定不变的规则涉及的数据表和事务边界是什么需要新增哪些接口、修改哪些已有接口这份设计说明不需要提交给任何人审阅它最大的价值是迫使你在写代码前把思路理清。很多返工其实不是因为代码写得差而是因为动手前没有想清楚边界和扩展点。6.2 设计文档应该写什么团队级的设计文档可以比个人笔记正式一些但也不要写成模板作文。有用的设计文档应该包含四个部分需求与目标本次设计要解决什么问题不解决什么问题。 现状分析当前代码结构、数据库表结构、核心调用链是什么。 设计方案模块划分、接口定义、数据模型变更、异常处理方案。 风险评估哪些逻辑可能变化哪些风险需要测试重点关注。这个结构既能指导开发也能给后来者提供上下文。文档不是越厚越好能够帮助下一任维护者快速理解系统就是一份成功的文档。6.3 用测试倒逼设计如果你不确定当前的代码设计是否合理有一个简单粗暴的检验方法尝试为它写单元测试。如果测试非常难写说明代码的耦合度过高如果测试可以轻松覆盖某个规则说明你已经把变化点隔离好了。在实际项目中我比较推荐在写实现之前先写一个面向行为的测试。测试描述的是“调用方期望看到什么行为”而不是“内部怎么实现”。这样写出来的代码自然会倾向于接口清晰、依赖简单。很多团队推不动单元测试问题不在于测试框架而在于源码本身没有为可测试性做设计。6.4 保持设计能力的几点建议设计能力不是看完一篇教程就能立刻提升的它需要持续练习。下面几个方法在团队和个人成长中都比较有效代码评审时不要只挑 bug多讨论职责和边界。问一句“这个方法放在这里是否合理”往往比“这个变量名拼错了”更有价值。主动重构一个老模块不要害怕改动。选择一个小范围、影响面可控的模块在测试的保护下完成重构体会从混乱到清晰的过程。阅读开源项目时不只看功能也看它的包结构和类职责划分。比如一些优秀的开源框架为什么会把某个能力抽象成接口为什么会有工厂和策略的配合这些都是设计层面的思考。建立自己的设计清单。每次写代码前默默过一遍这段逻辑的职责是否单一变化点有没有隔离调用方接口是否稳定数据一致性是否有保障问题越多说明越需要停下来想一想。还有一点值得反复提醒设计不是一次性的。好的设计也需要持续演进。你的系统会随着业务变化不断被修改设计会在每一次修改中被迫验证。如果发现原来的抽象已经不适合业务就勇敢地重新调整而不是为了让“设计好看”而继续保留一个不适用的结构。7. 总结与后续学习方向回到最初的问题我们是否忘记了如何设计答案是在追求快速交付的过程中很多团队确实丢掉了设计这个环节。但设计能力从来没有过时它只是在新的技术环境下换了一种表达方式。框架可以帮你管理对象生命周期脚手架可以帮你生成项目结构数据库中间件可以帮你处理分库分表但这些都无法替代你头脑中对业务边界的判断、对变化点的预判、对稳定接口的定义。从这次订单模块的例子可以看出设计与不设计的差别并不在于代码量的大小也不在于是否使用了什么设计模式而在于当需求变化来时你的修改是局部的还是全局的。策略模式只是手段真正的目标是把“可能变化的地方”和“稳定不变的地方”分离。如果你已经看完了这篇文章下一步建议你选择自己项目中一个经常变更的小模块尝试画一下它的职责边界找出里面最容易被修改的逻辑然后设计一个接口把它隔离出来。不要急着推翻整个系统就从一个点开始。当你体会到一次“修改只动了新增文件一行老代码没变”的快感时你就会重新找回设计的感觉。如果这篇文章对你有帮助可以收藏备用。也欢迎在评论区聊聊你在项目中遇到过哪些因为“没有设计”而踩下的坑我们下一次再见。
返回列表