接口抽象会提前复杂化

在企业软件领域,抽象(尤其是接口)被誉为优秀设计的标志。它们保证了灵活性、松耦合、可测试性,并遵循 SOLID 原则。我们在代码审查中推崇它们,在架构图中强制使用它们,并将它们不断注入到我们的应用程序中。

但不知从何时起,接口不再是一种手段,反而成了目的。它最初是用来管理复杂性的工具,如今却悄然成为一种条件反射。我们先写接口,再写实现。我们把简单的服务拆解成层层不必要的间接层。我们嘲笑这个世界,只是为了断言某个方法被调用了。而所有这一切都是以“良好设计”的名义进行的。

问题是什么?许多接口并没有实际用途。它们无法实现多态性,也无法保护我们免受变化的影响,也不会进化。它们只是存在——扰乱我们的项目,混淆我们的意图,拖慢我们的速度。本文挑战了对接口的盲目依赖。并非要完全否定抽象,而是要质疑它的作用。

我们要问:从这一层间接层中我们真正获得了什么?又在悄悄地失去什么?

// Interface
public interface AccountService {
    void createAccount(String username);
}

// Implementation
public class DefaultAccountService implements AccountService {
    private final AccountRepository accountRepository;

    public DefaultAccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Override
    public void createAccount(String username) {
        accountRepository.save(new Account(username));
    }
}

// Interface again
public interface AccountRepository {
    void save(Account account);
}

// Implementation again
public class JdbcAccountRepository implements AccountRepository {
    @Override
    public void save(Account account) {
       
// JDBC logic to save account
    }
}

这段代码在纸面上看起来很“干净”,到处都是接口,完全可注入,而且符合 SOLID 规范。但它的实际价值是什么呢?再想想吧。

  • 只有一个AccountService,也只有一个AccountRepository。没有使用或预期多态性。
  • 没有计划更改持久层(例如将 JDBC 与 JPA、文件或内存交换)。这种间接访问只是为了“以防万一”。
  • 现在,阅读本文的开发人员需要跳过四个文件才能追踪一个操作(createAccount)。这简直是脑力劳动,而且没有任何回报。

抽象慢慢添加
搞太复杂的东西——其实可以——也应该——后面再加进来

做软件有个很多人没注意到的真相:刚开始根本不用急着把设计搞完美。

咱们这行老有人觉得,好设计就得一开始把各种接口啊、规则啊、扩展点啊全定死——好像能未卜先知似的。但真实情况是,第一次搞出来的复杂设计基本都是错的。因为系统以后会变成啥样谁都不知道。哪部分需要灵活变动?哪部分会固定不变?这些刚开始根本看不出来。咱们唯一能确定的就是现在要解决的具体问题——就该从这入手。

记住:加东西永远比删东西简单。

如果一开始就写直白好懂的代码,只解决眼前问题,反而能把思路理清楚。等后面真的需要第二个版本,或者要加新测试的时候,那才是搞复杂设计的最好时机。这时候你不是在瞎猜,而是在解决实际问题。

这是最好的进化设计:

  1. 开始具体化——尽可能直接地编写代码。
  2. 发现分歧——当出现第二个变体时,提取一个界面。
  3. 稳定抽象——只有这样,界面才能反映真实的、经过测试的行为。

这种方法并不排斥抽象,只是会延迟抽象,直到有证据支持为止。这并非缺乏专业性,只是更加诚实。

何时需要接口抽象?
抽象很强大,但只有在有意识地使用时才有效。要知道何时引入接口或架构层,需要抛开教条,提出一些实际的问题:

1. 现在或即将有多种实施方案吗?
如果你需要处理多种行为(例如,内存、JDBC 和 REST 支持的存储库),那么接口就很有意义。它允许这些实现清晰地共存,并使系统能够在它们之间切换。

✅抽象是:当多条路径已经存在或正在明显出现时。

2. 该代码是否经常更改(并且与其消费者不同)?
当组件独立于使用它的代码发展时,抽象会创建一个有用的边界。例如,支付网关通常会受益于接口,因为每个提供商(Stripe、PayPal 等)的行为都不同,并且会独立变化。

✅对抽象说是:当波动性被孤立并且值得一份稳定的合约时。

3. 这是一个共同关注的问题还是跨领域的关注问题?
日志记录、缓存或指标等内容通常需要抽象,因为它们可以在许多情况下重复使用,并且可能在后台改变其行为。

✅抽象是:当分离有助于重用或执行通用策略时。

4. 我是否需要对其进行隔离测试?
如果必须在没有依赖项的情况下测试组件(例如,在服务测试中模拟存储库),抽象会有所帮助。但前提是测试实际依赖项的速度太慢、不稳定或需要外部支持(例如,文件系统、数据库)。

✅对抽象说“是”:当测试复杂性的权衡值得采用间接方式时。

5. 我是在解决一个真正的问题,还是在为一个假设的问题进行设计?
最重要的问题是:如果你只是因为“将来可能需要”而进行抽象,那你就是在推测。最好等到第二个用例证明这种需求。

❌拒绝抽象:当它受到恐惧、仪式或想象的规模驱动时。