50、代码坏味道:夸夸其谈未来性 - 实战移除不必要的抽象

说实话,我见过太多代码被“过度设计”毁掉的例子。今天聊的这个坏味道,叫“夸夸其谈未来性”。

什么意思呢?就是你在写代码时,总想着“万一将来要扩展呢”、“说不定以后会支持这个功能”。于是你提前加了一层抽象、一个接口、一个工厂类。结果呢?那个“将来”从来没来过。代码却变得又臭又硬。

我个人习惯是:只对当前需求做抽象,不为未来买单。你想想看,未来是不可预测的。你猜的需求,十有八九是错的。

什么是“夸夸其谈未来性”?

说白了,就是过度预测未来需求。你为“可能”会出现的场景,提前引入了不必要的复杂性。

举个例子。你只需要保存用户信息到数据库。结果你写了个 UserRepository 接口,又写了个 MysqlUserRepository 实现类。还加了个 UserCache 抽象层。为什么?因为“万一将来要换数据库呢”、“万一将来要加缓存呢”。

嗯,这里要注意:这种“万一”通常不会发生。就算发生了,到时候重构也来得及。你现在加的抽象,反而让代码更难理解、更难修改。

核心原则:YAGNI(You Aren't Gonna Need It)—— 你其实不需要它。

实战案例:一个过度抽象的订单系统

我在项目中遇到过这样一个案例。一个订单处理模块,只有两种订单:普通订单和会员订单。结果代码里却出现了这样的结构:

// 抽象层:订单处理器接口
public interface OrderProcessor {
    void process(Order order);
}

// 抽象层:订单验证器接口
public interface OrderValidator {
    boolean validate(Order order);
}

// 抽象层:订单通知器接口
public interface OrderNotifier {
    void notify(Order order);
}

// 实现类:普通订单处理器
public class NormalOrderProcessor implements OrderProcessor {
    private OrderValidator validator;
    private OrderNotifier notifier;
    
    public NormalOrderProcessor(OrderValidator validator, OrderNotifier notifier) {
        this.validator = validator;
        this.notifier = notifier;
    }
    
    @Override
    public void process(Order order) {
        if (validator.validate(order)) {
            // 处理订单逻辑
            notifier.notify(order);
        }
    }
}

// 实现类:会员订单处理器
public class VipOrderProcessor implements OrderProcessor {
    // 几乎一样的代码,只是折扣逻辑不同
    // ...
}

你发现问题了吗?三个接口,两个实现类,还有依赖注入。但实际业务逻辑只有十几行。这就是典型的“夸夸其谈未来性”。

我当时接手这个模块时,花了整整一天才理清这些抽象之间的关系。其实核心逻辑很简单:

// 重构后:一个类搞定
public class OrderService {
    public void processOrder(Order order) {
        // 1. 验证订单
        if (!validate(order)) {
            throw new IllegalArgumentException("订单无效");
        }
        
        // 2. 计算价格(普通订单和会员订单的区别就在这里)
        BigDecimal price = calculatePrice(order);
        
        // 3. 保存订单
        saveOrder(order, price);
        
        // 4. 发送通知
        sendNotification(order);
    }
    
    private BigDecimal calculatePrice(Order order) {
        if (order.isVip()) {
            return order.getAmount().multiply(new BigDecimal("0.9")); // 会员9折
        }
        return order.getAmount();
    }
    
    private boolean validate(Order order) {
        return order != null && order.getAmount() != null 
               && order.getAmount().compareTo(BigDecimal.ZERO) > 0;
    }
    
    private void saveOrder(Order order, BigDecimal price) {
        // 保存到数据库
    }
    
    private void sendNotification(Order order) {
        // 发送邮件或短信
    }
}

你看,去掉了所有不必要的接口和抽象,代码反而更清晰了。将来如果要支持第三种订单类型,直接在 calculatePrice 里加个分支就行。没必要提前搞什么策略模式。

如何识别“夸夸其谈未来性”?

我总结了几条判断标准:

症状 典型表现 严重程度
接口只有一个实现类 写了接口,但项目中只有这一个实现 ⚠️ 中等
抽象层没有实际调用者 工厂类、策略类写好了,但没人用 🔴 严重
为“将来”预留的扩展点 代码注释里写着“预留扩展” 🟡 轻微
过度使用设计模式 一个简单功能用了3个以上设计模式 🔴 严重

避坑指南:我曾经在一个项目中,看到有人为“将来可能支持多种支付方式”提前写了一个支付策略工厂。结果项目上线两年,始终只有微信支付一种方式。那个工厂类成了无人敢动的“僵尸代码”。每次修改支付逻辑,都要绕过那层抽象,直接改底层实现。你说这抽象有什么意义?

重构步骤:移除不必要的抽象

如果你发现代码里有这种坏味道,可以按以下步骤重构:

  1. 找出所有只有一个实现类的接口。这些接口大概率是多余的。
  2. 检查调用方。如果调用方只依赖接口,且只有一个实现,直接把接口和实现合并。
  3. 删除工厂类和配置类。如果工厂类只生产一种对象,直接 new 出来就行。
  4. 内联方法。把那些只有一行调用的方法,直接内联到调用处。
  5. 运行测试。确保重构后功能不变。

举个例子。假设你有这样的代码:

// 重构前
public interface PaymentService {
    void pay(BigDecimal amount);
}

public class WechatPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // 微信支付逻辑
        System.out.println("微信支付:" + amount);
    }
}

// 调用方
public class OrderController {
    private PaymentService paymentService;
    
    public OrderController() {
        this.paymentService = new WechatPaymentService();
    }
    
    public void checkout(BigDecimal amount) {
        paymentService.pay(amount);
    }
}

重构后:

// 重构后:去掉接口,直接使用实现类
public class WechatPaymentService {
    public void pay(BigDecimal amount) {
        // 微信支付逻辑
        System.out.println("微信支付:" + amount);
    }
}

public class OrderController {
    private WechatPaymentService paymentService;
    
    public OrderController() {
        this.paymentService = new WechatPaymentService();
    }
    
    public void checkout(BigDecimal amount) {
        paymentService.pay(amount);
    }
}

你看,代码少了三分之一,可读性却提高了。将来如果真的需要支持支付宝,再提取接口也不迟。到那时,你对支付逻辑的理解会更深入,提取的接口也会更合理。

什么时候可以保留抽象?

当然,我不是说所有抽象都是坏的。以下情况可以保留:

  • 当前就有多个实现。比如你同时支持 MySQL 和 Redis 两种存储。
  • 框架强制要求。比如 Spring 的接口注入。
  • 测试需要。你需要 mock 接口来写单元测试。
  • 团队约定。如果团队有统一的架构规范,可以遵守。

但记住:抽象是有成本的。每多一层抽象,就多一层理解成本、维护成本和调试成本。不要为了“优雅”而抽象,要为“解决实际问题”而抽象。

我的建议:写代码时,先写最直接的实现。等确实出现了重复代码或扩展需求,再考虑提取抽象。这叫“三次原则”——同一个逻辑出现三次,才考虑抽象。两次以内,复制粘贴都比抽象好。

核心知识体系

下面这张图总结了本章的核心逻辑:

夸夸其谈未来性 · 知识体系 核心问题:过度预测未来需求 典型症状 • 接口只有一个实现类 • 抽象层没有实际调用者 • 为“将来”预留扩展点 • 过度使用设计模式 重构步骤 1. 找出单实现接口 2. 合并接口与实现 3. 删除工厂类 4. 内联方法 重构结果 • 代码量减少30%-50% • 可读性显著提升 核心原则:YAGNI(You Aren't Gonna Need It)

总结一下:不要为未来写代码,要为现在写代码。未来是不可预测的,你猜的需求大概率是错的。等需求真的来了,再重构也不迟。到那时,你对业务的理解更深入,做出的抽象也更合理。

记住:好的代码不是“考虑周全”的代码,而是“容易修改”的代码。去掉不必要的抽象,就是让代码更容易修改的第一步。


公众号:蓝海资料掘金营,微信deep3321