资讯动态

代码抽象层次提升指南:以订单重构为例看策略模式与接口设计

发布时间:2026/10/13 13:45:03 来源:尧图企业网站定制
做了这么多年开发我越来越觉得“抽象层次”这四个字是面向对象实践里最值得反复琢磨的东西。它不像语法特性那样有标准答案也很少有人能明确告诉你“这一层该不该抽”“这一层是不是抽多了”但它几乎决定了代码是越改越顺还是越改越乱。我从一个实际案例说起把一段订单处理代码从“流水账”到“接口协作”再到“组织选择”的重构过程完整拆开给你看。这个过程能帮你搞清楚提升抽象层次到底是在提升什么、什么时候该做以及怎么做才能不掉进过度抽象的坑。如果你是把继承、多态、接口用得比较熟又总觉得系统哪里不对劲的开发者或者正在被别人吐槽代码难改、难测、难扩展的维护者这篇内容应该对你有用。1. 先理解“抽象层次”到底是在提升什么1.1 抽象层次回答的是“代码在说什么”抽象层次这个词听起来玄其实就是“代码在跟谁说话”。低抽象层次的代码通篇是“怎么算”if、for、类型判断、字段取值。高抽象层次的代码通篇是“算什么”下订单、应用折扣、发起支付。两者的差别决定了读代码的人要在细节里挣扎多久才能看出业务意图。举个例子。同样是“会员打八折”这个规则// 低抽象层次看到的是“怎么算” BigDecimal price product.getPrice(); if (user.getLevel() 2) { price price.multiply(new BigDecimal(0.8)); }// 高抽象层次看到的是“算什么” BigDecimal price discountPolicy.apply(user, product, originalPrice);第一段代码读者需要自己把“user.getLevel() 2”翻译成“会员”再把“乘以0.8”翻译成“八折”。第二段代码方法名直接表达了业务意图具体怎么折扣、判断条件是什么被放到了抽象边界后面。这就是抽象层次的提升让代码从“描述操作”变成“表达语义”。用生活里的例子类比更直观。一份菜谱可以有不同写法低抽象层次是“把黄瓜切成3毫米薄片加2克盐淋5毫升香油”高抽象层次是“拌一个凉菜”。家庭厨房需要前者因为执行者就是你自己连锁餐厅的后厨标准作业流程需要前者因为执行者可能是新手。但如果你是给厨师团队定菜单的人你需要的是后者——先把“做什么菜”定下来具体怎么切、怎么调味是厨师的事。代码也是这样。调用方需要的是“做什么”实现方负责“怎么做”。两者之间隔着的就是抽象层次。当调用方被迫看到“怎么做”的细节时抽象层次就太低了。1.2 抽象层次的核心价值把变化关在笼子里提升抽象层次最直接的价值不是让代码“好看”而是把变化关进一个小笼子里。想想支付渠道这个典型的业务变化点。今天只支持余额支付明天要加银行卡后天要加积分。如果订单服务直接依赖每个具体支付类那每新增一种渠道订单服务就要跟着改一遍。变化是爆炸式的一个渠道变了十处代码要跟着动。但如果订单服务只依赖一个“可支付”的抽象——比如一个接口或者一个策略——那新增渠道就只需要新增一个实现类然后把创建它的职责交给组装层。变化被关在了策略类的内部订单服务本身保持稳定。这就是“开闭原则”说的事情对扩展开放对修改关闭。不过我得先泼一盆冷水抽象不是越高级越好。每一层抽象都有代价更多的类、更多的间接调用、更多需要理解的概念。如果一个地方根本不会产生变化你做的抽象就只是提前支付利息。抽象层次的提升应当发生在看到重复和变化之后而不是建系统第一天就铺开一整套高级架构。这个度的把握后面专门讲。2. 识别抽象不足的四个信号2.1 信号一if-else 和 switch 像牛皮癣一样扩散最典型的就是根据类型字段一路判断下去。支付类型、订单类型、用户类型、通知渠道类型……每个地方都要写一遍if (BALANCE.equals(payType)) { // 扣余额 } else if (CARD.equals(payType)) { // 走银行卡 } else if (POINTS.equals(payType)) { // 扣积分 }这种代码意味着调用方需要知道所有支付方式的存在。每新增一种类型所有写了这个分支的地方都要跟着加一个else。你跟我说“就加一个分支很快”——今天加一个明天加一个三个月后这个if-else变成了几十行跟了一堆状态谁也看不懂为什么会有这么多种情况。抽象层次低的表现就是这样变化点没有被隔离到边界而是散落在业务代码里随着业务发展不断复制粘贴。2.2 信号二一个类什么都做注释都得写三行一个类或者一个方法职责多到需要用一段小作文式的注释来解释它是干什么的那通常就是抽象层次不够。比如订单服务同时管订单校验、折扣计算、库存扣减、支付、通知发送方法越来越长谁也说不清这个类的核心职责是什么。我见过不少代码一个类几十个方法字段交错依赖了一堆外部服务。你说它没有抽象吧它有类有方法你说它有抽象吧那类的抽象层次跟一个杂物间差不多——东西都能找到但你要在杂物间里翻很久才能拿到一把螺丝刀。真正合适的抽象是一个类或一个方法可以一句话说清楚这个策略负责计算折扣、这个工厂负责创建支付渠道、这个服务负责编排订单流程。如果一句话说不清楚或者要用一大段解释才能说明白就该考虑拆分了。2.3 信号三改需求变成了“牵一发而动全身”业务方说“会员折扣从八折改成九折”你发现要改三个类说“新增一种支付渠道”你发现要动订单服务、前端接口、测试用例、还有两处历史遗留的逻辑。这就是典型的抽象不足这个变化没有收敛到一个抽象边界里。改动波及范围越大说明变化点与稳定点没有分离开。一个成熟的设计应该是业务规则的变化尽量落在少数几个类里而不是穿透整个系统。如果你每次改需求都要在多个模块之间跳来跳去先别急着骂需求不稳定回头看看是不是抽象层次没到位。2.4 信号四单测只能绕过真业务、打一堆桩写单元测试的时候如果只能new一个具体实现类没法用Mock替换依赖那说明调用方和实现方耦合太紧。你想要测订单服务的折扣逻辑结果它内部直接依赖了具体支付器、具体库存系统、具体用户服务你被迫把整条链路都模拟出来。测试复杂本质上是抽象层次不足的副作用。面向接口编程做得好测试也自然轻松测OrderService时传一个Mock的DiscountStrategy、Mock的PayStrategy逻辑验证干净利落。测试难写不是测试的问题是设计的问题——抽象边界没有把稳定逻辑和易变依赖分开。这四个信号你在自己的项目里对照一下。如果中了两条以上就该认真考虑提升抽象层次了。3. 实战复盘订单处理Demo的三次重构3.1 版本一业务全在流水账里先看一段典型的“能跑但让人头疼”的代码。一个订单服务把折扣规则和支付方式全部写在checkout方法里public class OrderService { public BigDecimal checkout(Order order, User user, String payType) { BigDecimal amount order.getTotalAmount(); if (user.isMember()) { if (order.getPromotionType() 1) { amount amount.multiply(new BigDecimal(0.85)); } else { amount amount.multiply(new BigDecimal(0.95)); } } if (BALANCE.equals(payType)) { user.reduceBalance(amount); } else if (CARD.equals(payType)) { cardPayService.pay(order.getId(), amount); } else if (POINTS.equals(payType)) { int points amount.intValue() * 100; user.reducePoints(points); } // 扣库存、写订单状态、发通知…… return amount; } }这段代码的运行逻辑很好懂问题在于它把所有事情都混在一起。第一调用方被迫看到所有分支第二每加一种支付方式方法体就膨胀一层第三折扣规则和支付方式纠缠在一起没法单独测试某一个支付逻辑第四这段代码几乎没有复用性另一个模块想用同样的折扣规则只能复制粘贴。这属于抽象层次还停留在“具体实现”的层面。整个方法就是一段流水账把流程从头到尾记了一遍没有形成任何有语义的中间抽象。3.2 版本二用接口把算法抽象成策略第一次重构先解决最痛的问题把折扣计算和支付方式从大方法里分离出来。先定义支付策略的接口public interface PayStrategy { boolean supports(String payType); void pay(Order order, BigDecimal amount, User user); }然后分别实现余额支付、银行卡支付、积分支付public class BalancePayStrategy implements PayStrategy { Override public boolean supports(String payType) { return BALANCE.equals(payType); } Override public void pay(Order order, BigDecimal amount, User user) { user.reduceBalance(amount); } } public class CardPayStrategy implements PayStrategy { // 调用银行卡支付接口 } public class PointsPayStrategy implements PayStrategy { // 积分抵扣逻辑 }折扣规则也做相同的处理public interface DiscountStrategy { boolean supports(Order order, User user); BigDecimal apply(Order order, User user, BigDecimal original); }这时候订单服务变成这样public class OrderService { private final ListPayStrategy payStrategies; private final ListDiscountStrategy discountStrategies; public OrderService(ListPayStrategy payStrategies, ListDiscountStrategy discountStrategies) { this.payStrategies payStrategies; this.discountStrategies discountStrategies; } public BigDecimal checkout(Order order, User user, String payType) { DiscountStrategy discount discountStrategies.stream() .filter(s - s.supports(order, user)) .findFirst() .orElseThrow(() - new IllegalArgumentException(No discount strategy found)); BigDecimal amount discount.apply(order, user, order.getTotalAmount()); PayStrategy payment payStrategies.stream() .filter(s - s.supports(payType)) .findFirst() .orElseThrow(() - new IllegalArgumentException(Unsupported payType: payType)); payment.pay(order, amount, user); return amount; } }抽象层次提升在哪里checkout不再知道“怎么打折”它只关心“找到合适的折扣策略并执行”它也不再知道“怎么支付”只关心“找到支持该类型的支付策略”。这个版本已经很有价值新增支付渠道只需要新增一个类注入到策略列表订单服务本身不用改。新增折扣规则同理。测试也变干净了——给OrderService传一个Mock的PayStrategy列表一测一个准。3.3 版本三把“选择”这件事继续抽象版本二还有一个没有完全解决的问题策略列表谁来组装如果每个调用方都要自己维护一份策略列表新增渠道时可能漏掉某处组装策略注册的逻辑还是散的。于是再往上抽象一层把“选择策略”这件事集中到一个工厂或注册表里。public class PayStrategyFactory { private static final MapString, PayStrategy STRATEGIES new HashMap(); static { register(BALANCE, new BalancePayStrategy()); register(CARD, new CardPayStrategy()); register(POINTS, new PointsPayStrategy()); } public static void register(String payType, PayStrategy strategy) { STRATEGIES.put(payType, strategy); } public static PayStrategy get(String payType) { PayStrategy strategy STRATEGIES.get(payType); if (strategy null) { throw new IllegalArgumentException(Unsupported payType: payType); } return strategy; } }订单服务再次瘦身public class OrderService { private final PayStrategyFactory payStrategyFactory; private final DiscountStrategyFactory discountStrategyFactory; public OrderService(PayStrategyFactory payStrategyFactory, DiscountStrategyFactory discountStrategyFactory) { this.payStrategyFactory payStrategyFactory; this.discountStrategyFactory discountStrategyFactory; } public BigDecimal checkout(Order order, User user, String payType) { BigDecimal amount discountStrategyFactory.get(order, user).apply(order, user, order.getTotalAmount()); payStrategyFactory.get(payType).pay(order, amount, user); return amount; } }这层抽象的意义在于订单服务不再关心策略从哪来、有哪些策略它只依赖一个确定能力——根据输入找到正确的策略。策略的注册集中在一个地方新增渠道时改动收敛到Factory内部。如果你继续往上抽象可能会把支付和折扣组合成一条处理链路甚至引入领域事件让订单流程拆成多个独立处理器。但那是另一个层级的架构问题了不是每个项目都需要。3.4 重构前后对比维度版本一版本二版本三新增支付渠道改OrderService加if分支新增类注入策略列表新增类注册到Factory新增折扣规则改OrderService加if分支新增类注入策略列表新增类注册到FactoryOrderService职责知道一切逻辑细节知道如何选择策略只依赖抽象工厂单元测试很难需构造完整依赖链可以Mock策略列表直接Mock工厂逻辑清晰阅读成本一个方法讲完所有事方法很长需要看接口和多个实现类需要理解工厂注册表这三次重构每一次抽象层次都往上走了一层。第一次是“算法策略化”第二次是“选择过程集中化”变化点被一层层关进更小的笼子里。代码变多了但维护成本降下来了。4. 提升抽象层次的可操作方法4.1 找到变化点抽象要长在变化点上抽象不是从零开始设计出来的是从具体代码里“长”出来的。那怎么判断长在哪里先找变化点。我自己常用的办法是问三个问题业务里哪些东西经常在变哪些东西是稳定的核心流程哪些外部依赖可能会替换答案指向哪里哪里就是值得抽象的地方。常见的变化点包括外部服务或渠道支付、短信、通知、第三方接口、业务规则折扣、校验、计算、状态流转、数据来源配置、模板、策略参数。在这些位置做抽象收益最大。反过来数据库连接方式、基础工具方法这些相对稳定的东西抽象来抽象去就是自己给自己找事。4.2 面向接口而不是面向实现“面向接口编程”这句话被说烂了但很多人的理解只停留在“定义一个接口然后实现它”。真正的核心是调用方需要依赖抽象不依赖具体类。写调用方代码时要问“我需要的是一种什么能力”而不是“我现在要new哪个类”。回到订单服务的例子。checkout需要的是什么不是“BalancePayStrategy”这个类而是“能根据订单和金额完成支付”的能力。这种能力就是PayStrategy接口。定义接口时想的是“调用方的需求”而不是“我有的实现”。方向反了接口就会设计得绑手绑脚。4.3 组合优于继承让对象协作继承适合“is-a”的关系猫是一种动物圆形是一种形状。但业务里的变化关系大多不是“是什么”而是“能做什么”或者“拥有什么”。会员订单不是“订单的一种”而是“订单拥有一个折扣规则”。这种情况下用继承去表达就会很别扭。比如有人会把类设计成“MemberOrderService extends OrderService”来实现会员折扣。短期看挺省事但折扣规则一变继承体系就要跟着改。更好的做法是把折扣规则作为OrderService协作对象注入进去通过组合让对象协作。策略模式、装饰器模式都是组合思想的体现。组合的另一个好处是测试方便替换一个合作者比新建一个子类轻量得多。4.4 把创建和使用拆开引入中间层一个对象被创建出来和它被使用其实是两件事。版本三的工厂做的事情就是这个把“创建哪个策略”从“如何使用策略”中拆了出来。这个拆分本身就是抽象层次的提升。依赖注入、工厂、服务定位器本质都是在做同一件事让使用者不关心创建过程。抽象层次越高越多的“如何组装依赖”会被推到系统边界。你可能听过“控制反转”这个词它想表达的就是不该由核心业务代码去决定依赖怎么来这个决定权应该交给更上层的组装逻辑。5. 不要掉进抽象陷阱5.1 过度抽象代码变成了迷宫提升抽象层次的反面是抽象到没人能看懂。我见过一些代码为了“保险”把订单、用户、库存全设计成接口实现工厂的三件套结果改一个字段要改五个文件加一个参数要改三次签名。这不是抽象这是给自己设障碍。判断抽象是否过度的标准很简单抽象有没有让变化更快如果加一个需求反而要动更多文件、绕更多弯那就是抽象过重了。抽象是手段不是目的。不要为了让代码看起来“高级”而去套设计模式。5.2 提前抽象猜需求而不是响应需求过早抽象比过度抽象更隐蔽。项目刚起步需求还不明朗就把Strategy、Factory、Builder全部铺开美其名曰“架构设计”。结果半年过去变化点根本没出现白付了一堆成本还拖慢了开发节奏。我自己的经验是至少看到两次重复再考虑抽象。第一次重复可能是巧合第二次往往是趋势。抽象应该是“渐进提升”的而不是“一步到位”的。等你真的遇到变化再让抽象层次往上走完全来得及。代码重构没那么可怕关键是每一步都有测试兜底。5.3 抽象位置放错该抽象的地方不抽象还有一种常见问题该抽象的地方不抽象不该抽象的地方疯狂抽象。比如支付渠道每年换三回无人处理数据库访问方式稳定五六年却包了五六层。抽象优先级应该跟“需求变动频率”对齐。变动频繁的地方值得多投一些设计成本稳定不变的地方简单直接就是最好的设计。我每次做代码评审都会留意改动最多的模块和最稳定的模块。改动多的模块如果设计混乱那就是抽象层次没跟上稳定模块如果过度设计那就是抽象放错了地方。这条思路比单纯背设计原则更有助于定位问题。6. 代码评审与日常落地中的检查清单6.1 评审时多问三个问题以后评审代码时除了语法和逻辑多问自己三个问题。第一这个改动是让旧代码变得更简单了还是只是打了补丁第二如果我今天要新增一个同类型功能需要改几个文件理想情况是一个文件最多两个。第三这次用到的抽象是为一个已经确认的变化准备的还是为了“万一以后”第三个问题最容易暴露问题。如果你答“万一以后”那大概率是在提前抽象先放一放不丢人。6.2 用小步重构代替大重写提升抽象层次不一定要推倒重来。最稳的路径是保持行为不变小步重构。先提取常量再提取方法再提取类最后再决定要不要提取接口。每一步都保持编译通过、测试通过再走下一步。举个例子你想把版本一的checkout拆成策略模式先在原方法里把折扣计算提取成一个私有方法测试通过后再把这个方法移到独立类再测试通过最后再定义接口。这样每一步都安全不会走到一半发现整个模块崩了。6.3 把抽象决策写下来代码里那些“故意不抽象”的地方一定要留下注释说明理由。比如“这里刻意不提取接口因为目前只有一种实现等出现第二种时再重构”。这段注释能阻止后来的人为了“规范”而过度设计。同样做抽象的地方也可以写一句为什么选择这个抽象层次、它隔离的是什么变化、判断它是否需要继续深化的信号是什么。别小看这种记录半年后回来看代码你会感谢自己当初写下的这一段话。我个人这些年做下来最大的体会是抽象层次不是写代码之前想出来的而是重构重构再重构之后“长”出来的。好的抽象一定不早、不晚、不贵。你只要每次改需求都留一分心思审视一下——这里是不是又在重复了这个类是不是又超载了能不能把变化关进一个小盒子里——抽象层次就会一步一步往上走。你的代码不会一夜之间变成教科书但一年之后回头看你会发现自己已经很少写那种连自己都要琢磨半天的流水账了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑