为什么使用接口 + 工厂?让一切都可替换的 Java 模式
Source: Dev.to
模式
每个组件——用例、处理步骤、适配器——都被定义为带有静态工厂方法的接口:
public interface ProcessOrder {
record Request(String orderId, String paymentToken) {}
record Response(OrderConfirmation confirmation) {}
Result execute(Request request);
interface ValidateInput {
Result apply(Request raw);
}
interface ReserveInventory {
Result apply(ValidRequest req);
}
interface ProcessPayment {
Result apply(Reservation reservation);
}
interface ConfirmOrder {
Result apply(Payment payment);
}
static ProcessOrder processOrder(
ValidateInput validate,
ReserveInventory reserve,
ProcessPayment processPayment,
ConfirmOrder confirm) {
return request -> validate.apply(request)
.flatMap(reserve::apply)
.flatMap(processPayment::apply)
.flatMap(confirm::apply);
}
}
四个步骤。每个都是单方法接口。工厂方法接受所有依赖作为参数,并返回实现该用例的 lambda。方法体的阅读顺序正好像业务流程:validate → reserve → process payment → confirm。
这并非随意的约定。该结构存在三个具体原因。
Reason 1: Substitutability Without Magic
Anyone can implement the interface—no framework, no inheritance hierarchy, no annotations.
Testing becomes trivial
@Test
void order_fails_when_inventory_insufficient() {
var useCase = ProcessOrder.processOrder(
request -> Result.success(new ValidRequest(request)), // always valid
req -> INSUFFICIENT_INVENTORY.result(), // always fails
reservation -> { throw new AssertionError("unreachable"); },
payment -> { throw new AssertionError("unreachable"); }
);
useCase.execute(new Request("order-1", "tok_123"))
.onSuccess(Assertions::fail);
}
没有 mocking 框架,没有 @Mock 注解,也没有 when().thenReturn() 链。测试通过普通 lambda 构造出它所需要的精确场景。
Stubbing incomplete implementations during development
// Payment gateway isn’t ready yet? Stub it.
var useCase = ProcessOrder.processOrder(
realValidator,
realInventoryService,
reservation -> Result.success(new Payment("stub-" + reservation.id(), Money.ZERO)),
realConfirmation
);
负责库存的团队无需等待支付团队的实现。每一步都可以独立实现。
原因 2:实现隔离
每个实现都是自包含的。没有共享的基类,没有需要覆盖的抽象方法,实现之间根本没有耦合。
与典型抽象类方式的对比
// The abstract‑class trap
public abstract class AbstractOrderProcessor {
protected final Logger log = LoggerFactory.getLogger(getClass());
public final Result execute(Request request) {
log.info("Processing order: {}", request.orderId());
var result = doExecute(request);
log.info("Order result: {}", result);
return result;
}
protected abstract Result doExecute(Request request);
protected abstract Result validate(Request request);
// “Shared utility” that every subclass now depends on
protected Result calculateTotal(List items) {
// 47 lines of logic that one subclass needed once
}
}
每个实现都会耦合到基类。修改 calculateTotal 需要了解所有子类。向 execute 添加日志会注入到每个实现中,无论是否合适。基类变成了一个引力井——累积共享代码,产生了本应毫不相干的实现之间的隐形依赖。
使用接口 + 工厂
没有共享的实现代码。就是这么简单。实现之间的每一次交叉都是不必要的耦合,带来相应的维护开销——甚至需要深入了解两个项目而不是一个,却没有任何好处。
// Implementation A: uses database
static ProcessPayment databasePayment(PaymentRepository repo) {
return reservation -> repo.charge(reservation.paymentToken(),
reservation.total())
.map(Payment::fromRecord);
}
// Implementation B: uses external API
static ProcessPayment stripePayment(StripeClient client) {
return reservation -> client.createCharge(reservation.total(),
reservation.paymentToken())
.map(Payment::fromStripe);
}
这些实现彼此互不知晓。它们不共享代码或基类。它们仅共享一个契约——接口,除此之外别无他物。
原因 3:一次性实现
细微之处在于:工厂方法返回的是一个 lambda(或本地记录)。它不能通过类名在外部引用,这鼓励 组合优于继承,并且可以轻松丢弃或替换实现,而不会影响公共 API。
// Example: a one‑off implementation used only in a specific test
var testUseCase = ProcessOrder.processOrder(
validator,
inventory,
reservation -> Result.success(new Payment("test-" + reservation.id(),
Money.of(0))),
confirmer
);
因为实现仅以 lambda 形式存在,避免了意外的复用或隐藏的耦合。当需求变化时,只需提供不同的 lambda 或工厂方法即可。
TL;DR
- 接口 + 工厂 为你提供:
- 纯粹的可替换性 – 任意实现都可以在不依赖框架的情况下互换。
- 隔离性 – 实现之间不共享隐藏的基类依赖。
- 一次性、可组合的实现 – 易于存根、测试和替换。
采用此模式可保持你的 Java 代码库模块化、可测试,并摆脱传统继承繁重设计中常见的“引力井”问题。
ProcessOrder – 函数式组合示例
static ProcessOrder processOrder(ValidateInput validate,
ReserveInventory reserve,
ProcessPayment processPayment,
ConfirmOrder confirm) {
return request -> validate.apply(request) // 这个 lambda 是实现本身
.flatMap(reserve::apply)
.flatMap(processPayment::apply)
.flatMap(confirm::apply);
}
无直接实例化
没有代码写成 new ProcessOrderImpl()。
没有其他代码依赖具体的实现类。
因为没有任何东西可以引用它,实现可以被完全替换。
- 接口是设计产出。
- 实现是偶然的。
为什么这很重要
你可能会觉得这只是学术性的讨论,直到你需要:
- 用异步实现替换同步实现。
- 将数据库适配器换成 API 适配器。
- 在现有步骤周围添加缓存层。
- 完全重写某一步的内部实现。
在每种情况下:
- 接口保持不变。
- 工厂方法的签名保持不变。
- 实现(没有任何引用)被替换掉。
结果:无需下游更改,无需适配层,也没有“向后兼容” 的麻烦。
复合效应
每个收益单独来看都很有价值,但它们共同构成了一个系统,使得:
测试即配置
组装你需要的真实组件和存根组件的精确组合。
- 没有模拟框架的开销。
- 没有 “全部模拟” 导致的测试脆弱性。
重构是安全的
替换实现不会破坏其他实现,因为它们不共享代码。
- 编译器通过接口强制执行契约。
复杂度受限
理解一个实现只需要了解该实现本身以及它所使用的接口。
- 没有深层类层次结构。
- 没有耦合实现的共享工具。
增量开发自然进行
- 对尚未准备好的部分使用存根。
- 一次替换一个存根为真实实现。
- 每一步都可以独立开发、测试和部署。
何时不适用?
当确实只有 一种实现且永远只有一种——例如纯工具函数、数学计算、简单的数据转换时。在这些情况下,静态方法完全合适。
只要 存在任何可能的多种实现(包括几乎总是存在的测试实现),该模式就能收回成本。
转变
大多数 Java 代码库默认使用具体类。接口的抽取往往在后期才进行,往往是被动的——要么因为测试的需求,要么因为出现了第二个实现。
翻转这种做法:
- 从接口开始。 首先定义契约。
- 稍后实现。 实现自然随之而来,而且当需要更改时,其他内容都不受影响。
接口是你设计的内容。
实现是你今天恰好写的代码。
