设计模式详解-适配器模式

设计模式详解:适配器模式

一、模式概述

适配器模式(Adapter Pattern)是结构型设计模式中最具工程实用价值的模式之一,其核心意图在于将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以协同运作。这一模式的命名源自现实生活中的电源适配器——不同国家的电压标准各异,适配器作为中间层,将异国电源转换为本国设备可接受的规格,而无需改造设备本身的电路设计。

在软件系统的演进历程中,接口不兼容是永恒的主题。遗留系统的接口与现代框架不匹配、第三方库的API风格与项目规范冲突、微服务拆分后协议差异需要弥合、多团队协作时的契约不一致——这些场景每天都在上演。适配器模式的价值不在于创造新功能,而在于以最小的侵入性成本,打通异构系统之间的连接通道,保护既有投资,延缓重构决策。

适配器模式的深层哲学是"拥抱差异,而非消灭差异"。它承认系统异构性的客观存在,通过引入薄层的转换逻辑,在不破坏原有封装的前提下实现互联互通。这一思想与微服务架构中的API网关、企业集成模式中的消息转换器、云原生生态中的服务网格等现代基础设施一脉相承。

二、模式结构

适配器模式存在两种实现形态,分别适用于不同的类结构场景:

对象适配器(Object Adapter):基于组合实现,适配器持有被适配者的实例引用,通过调用其实现目标接口。这是更常用的形态,遵循"优先使用组合而非继承"的设计原则。

类适配器(Class Adapter):基于多重继承实现,适配器同时继承目标接口和被适配者类。受限于Java等单继承语言,实际应用较少,但在C++等支持多重继承的语言中有其用武之地。

两种形态共享以下角色:

目标接口(Target):客户端期望使用的标准接口,定义了业务所需的操作集合。

被适配者(Adaptee):已存在的、具有有用功能但接口不兼容的类或组件。

适配器(Adapter):核心转换层,实现目标接口,内部调用被适配者的功能,完成接口映射与语义转换。

客户端(Client):通过目标接口与适配器交互,无需感知被适配者的存在。

三、深度案例:遗留系统现代化改造

以下展示一个真实场景下的适配器模式应用——某金融企业核心银行系统的现代化改造,需要将上世纪90年代开发的COBOL批处理模块集成至基于Spring Cloud的微服务架构。

3.1 遗留系统:COBOL批处理接口

核心账务系统的批量记账模块以CICS事务网关暴露服务,接口风格与现代RESTful实践格格不入:

java
/** * 遗留系统接口:COBOL批处理网关 * 通过CTG(CICS Transaction Gateway)调用 */ public class CobolBatchGateway { // 原始接口:使用固定位置字段的COBOL copybook格式 public byte[] executeBatchTransaction(byte[] cobolInput) throws CobolException { // 通过CTG连接CICS区域 ECIRequest eciRequest = new ECIRequest(); eciRequest.setCicsServer("PROD_REGION"); eciRequest.setProgramName("BATTXN01"); eciRequest.setCommarea(cobolInput); // 同步阻塞调用,超时时间固定为30秒 CTGManager.getInstance().flow(eciRequest); return eciRequest.getCommarea(); } // 原始接口:基于返回码的错误处理,无异常体系 public int getLastReturnCode() { return CTGManager.getInstance().getLastEciRc(); } // 原始接口:十六进制转储日志 public void dumpTransactionLog(String tranId) { // 写入z/OS数据集... } } // COBOL copybook对应的Java数据绑定 public class BatchTransactionInput { private String accountNumber; // PIC X(16) private BigDecimal amount; // PIC 9(15)V99 COMP-3 private String currencyCode; // PIC X(3) private String transactionType; // PIC X(4) private String referenceNumber; // PIC X(20) private String operatorId; // PIC X(8) private String branchCode; // PIC X(4) private String filler; // PIC X(125) 对齐用 // 转换为COBOL COMMAREA字节数组 public byte[] toCommarea() { ByteBuffer buffer = ByteBuffer.allocate(180); buffer.put(accountNumber.getBytes(EBCDIC)); // COMP-3 packed decimal编码... buffer.put(encodePackedDecimal(amount, 17, 2)); // 其余字段... return buffer.array(); } }

3.2 目标接口:现代微服务契约

新核心系统采用领域驱动设计,定义了清晰的限界上下文和防腐层接口:

java
/** * 目标接口:批量记账服务 * 符合现代DDD实践与Reactive编程模型 */ public interface BatchPostingService { // 同步版本:用于简单场景 PostingResult postBatch(BatchPostingRequest request); // 异步版本:用于高吞吐场景,返回Mono以支持背压 Mono<PostingResult> postBatchAsync(BatchPostingRequest request); // 批量版本:支持大文件拆分后的并行处理 Flux<PostingResult> postBatches(List<BatchPostingRequest> requests); // 补偿接口:支持Saga分布式事务 Mono<Void> compensate(String transactionId); } // 领域模型:批量记账请求 public class BatchPostingRequest { private TransactionId transactionId; private AccountId debitAccount; private AccountId creditAccount; private Money amount; private TransactionType type; private String reference; private Operator operator; private LocalDateTime valueDate; private Map<String, Object> extensions; // 建造者模式创建 public static Builder builder() { return new Builder(); } } // 领域模型:记账结果 public class PostingResult { private TransactionId transactionId; private PostingStatus status; private Money postedAmount; private Balance newBalance; private List<PostingEntry> entries; private Instant timestamp; private String traceId; }

3.3 适配器实现:多层转换与韧性设计

java
/** * 对象适配器:COBOL批处理网关 -> 现代批量记账服务 * 集成熔断、限流、日志、监控等企业级特性 */ @Component public class CobolBatchPostingAdapter implements BatchPostingService { private final CobolBatchGateway legacyGateway; private final CobolCopybookMapper copybookMapper; private final MeterRegistry meterRegistry; private final CircuitBreaker circuitBreaker; // 线程隔离:COBOL调用使用独立线程池,防止阻塞Netty事件循环 private final Scheduler cobolScheduler = Schedulers .fromExecutor(Executors.newFixedThreadPool(20, new ThreadFactory() { private final AtomicInteger counter = new AtomicInteger(); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "cobol-adapter-" + counter.incrementAndGet()); t.setDaemon(true); return t; } })); @Autowired public CobolBatchPostingAdapter( CobolBatchGateway legacyGateway, CobolCopybookMapper copybookMapper, MeterRegistry meterRegistry, CircuitBreakerRegistry cbRegistry) { this.legacyGateway = legacyGateway; this.copybookMapper = copybookMapper; this.meterRegistry = meterRegistry; this.circuitBreaker = cbRegistry.circuitBreaker("cobol-posting"); } @Override public PostingResult postBatch(BatchPostingRequest request) { // 指标记录 Timer.Sample sample = Timer.start(meterRegistry); try { // 1. 领域模型 -> COBOL copybook转换 BatchTransactionInput cobolInput = copybookMapper.toCobol(request); // 2. 调用遗留系统(带熔断保护) byte[] cobolOutput = circuitBreaker.executeSupplier(() -> { try { return legacyGateway.executeBatchTransaction( cobolInput.toCommarea() ); } catch (CobolException e) { throw new LegacySystemException("COBOL调用失败", e); } }); // 3. 返回码检查与异常转换 int returnCode = legacyGateway.getLastReturnCode(); if (returnCode != 0) { throw mapCobolError(returnCode, request.getTransactionId()); } // 4. COBOL copybook -> 领域模型转换 BatchTransactionOutput output = copybookMapper.fromCobol(cobolOutput); PostingResult result = mapToDomain(output, request); // 成功指标 meterRegistry.counter("cobol.posting.success").increment(); sample.stop(meterRegistry.timer("cobol.posting.latency")); return result; } catch (LegacySystemException e) { meterRegistry.counter("cobol.posting.failure").increment(); throw e; } } @Override public Mono<PostingResult> postBatchAsync(BatchPostingRequest request) { // 异步适配:将同步阻塞的COBOL调用包装为Reactive流 return Mono.fromCallable(() -> postBatch(request)) .subscribeOn(cobolScheduler) // 隔离到专用线程池 .timeout(Duration.ofSeconds(45)) // 覆盖COBOL的30秒固定超时 .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) .filter(e -> e instanceof LegacySystemException) .doAfterRetry(signal -> meterRegistry.counter("cobol.posting.retry").increment() )) .onErrorMap(TimeoutException.class, e -> new PostingTimeoutException(request.getTransactionId(), e)) .doOnEach(signal -> { // 分布式追踪上下文传递 if (signal.isOnNext()) { Tracing.currentTracer().nextSpan() .name("cobol-async-complete") .tag("transaction.id", request.getTransactionId().toString()) .start() .finish(); } }); } @Override public Flux<PostingResult> postBatches(List<BatchPostingRequest> requests) { // 批量适配:控制并发度,防止压垮遗留系统 return Flux.fromIterable(requests) .flatMap(this::postBatchAsync, 5) // 最大并发5个 .onErrorContinue((error, obj) -> { // 部分失败继续处理,记录死信 log.error("批量记账单项失败: {}", obj, error); meterRegistry.counter("cobol.posting.partial.failure").increment(); }); } @Override public Mono<Void> compensate(String transactionId) { // 补偿适配:COBOL系统无原生补偿,通过冲正交易实现 ReversalInput reversal = new ReversalInput(); reversal.setOriginalTransactionId(transactionId); return Mono.fromCallable(() -> { legacyGateway.executeBatchTransaction(reversal.toCommarea()); return legacyGateway.getLastReturnCode(); }) .subscribeOn(cobolScheduler) .flatMap(rc -> rc == 0 ? Mono.empty() : Mono.error(new CompensationFailedException(transactionId, rc))); } // 私有辅助方法 private RuntimeException mapCobolError(int returnCode, TransactionId txId) { switch (returnCode) { case 1: return new InsufficientBalanceException(txId); case 2: return new AccountNotFoundException(txId); case 3: return new DuplicateTransactionException(txId); case 4: return new PostingClosedException(txId); default: return new UnknownPostingException(txId, returnCode); } } private PostingResult mapToDomain(BatchTransactionOutput cobolOutput, BatchPostingRequest request) { return PostingResult.builder() .transactionId(request.getTransactionId()) .status(PostingStatus.COMPLETED) .postedAmount(request.getAmount()) .newBalance(new Balance(cobolOutput.getNewBalance(), request.getAmount().getCurrency())) .entries(extractEntries(cobolOutput)) .timestamp(Instant.now()) .traceId(MDC.get("traceId")) .build(); } }

3.4 双向适配器:遗留系统调用新服务

现代化改造往往是双向的:新系统需要调用遗留模块,遗留模块也需要消费新服务。以下展示反向适配:

java
/** * 反向适配器:使COBOL程序能够调用Java微服务 * 通过CICS的JVM COBOL互操作特性实现 */ @CicsProgram("NEWSVC01") public class NewServiceCobolAdapter { @Autowired private AccountQueryService accountQueryService; @Autowired private CustomerService customerService; /** * CICS LINK调用的入口点 * COMMAREA格式与COBOL copybook严格对应 */ public void handleCommarea(CommareaHolder commarea) { // 解析COBOL输入 CustomerQueryInput input = parseInput(commarea.getInput()); // 调用现代服务 CustomerProfile profile = customerService .findById(new CustomerId(input.getCustomerNumber())) .block(Duration.ofSeconds(10)); // 同步等待,适配COBOL的同步模型 // 转换为COBOL输出格式 CustomerQueryOutput output = toCobolFormat(profile); commarea.setOutput(output.toBytes()); } private CustomerQueryOutput toCobolFormat(CustomerProfile profile) { CustomerQueryOutput output = new CustomerQueryOutput(); // 字段映射与截断处理 output.setCustomerName(truncate(profile.getFullName(), 40)); output.setAddressLine1(truncate(profile.getPrimaryAddress().getLine1(), 35)); output.setAddressLine2(truncate(profile.getPrimaryAddress().getLine2(), 35)); // 枚举值转换:Java枚举 -> COBOL代码 output.setCustomerType(mapCustomerType(profile.getSegment())); output.setRiskRating(mapRiskRating(profile.getRiskScore())); // 日期格式转换:ISO-8601 -> YYYYMMDD output.setOpenDate(formatCobolDate(profile.getAccountOpenDate())); // 嵌套列表扁平化:COBOL不支持动态数组 List<AccountSummary> accounts = profile.getAccounts(); output.setAccountCount(Math.min(accounts.size(), 10)); // 最多10条 for (int i = 0; i < output.getAccountCount(); i++) { output.setAccountEntry(i, flattenAccount(accounts.get(i))); } return output; } }

四、适配器模式的高级形态

4.1 适配器链与管道

复杂场景下,单一适配器不足以完成转换,需要多个适配器串联形成处理管道:

java
@Component public class AdapterPipeline { private final List<ProtocolAdapter> adapters; public AdapterPipeline(List<ProtocolAdapter> adapters) { this.adapters = adapters.stream() .sorted(Comparator.comparingInt(ProtocolAdapter::getPriority)) .collect(Collectors.toList()); } public Message adapt(Message source, Protocol targetProtocol) { Message current = source; for (ProtocolAdapter adapter : adapters) { if (adapter.canHandle(current.getProtocol(), targetProtocol)) { current = adapter.adapt(current); if (current.getProtocol() == targetProtocol) { return current; } } } throw new NoAdapterPathException(source.getProtocol(), targetProtocol); } } // 具体适配器:ISO 8583 -> JSON @Component public class Iso8583ToJsonAdapter implements ProtocolAdapter { @Override public boolean canHandle(Protocol from, Protocol to) { return from == Protocol.ISO8583 && to == Protocol.JSON; } @Override public Message adapt(Message message) { Iso8583Message iso = (Iso8583Message) message; JsonMessage json = new JsonMessage(); // MTI映射 json.set("messageType", mapMti(iso.getMti())); // 位图展开 BitSet bitmap = iso.getBitmap(); for (int i = 1; i <= 128; i++) { if (bitmap.get(i)) { IsoField field = iso.getField(i); json.set("field" + i, convertField(field)); } } return json; } @Override public int getPriority() { return 100; } }

4.2 动态代理适配器

利用JDK动态代理或字节码生成,在运行时创建适配器,避免样板代码:

java
public class DynamicAdapterFactory { @SuppressWarnings("unchecked") public static <T> T createAdapter(Object adaptee, Class<T> targetInterface) { return (T) Proxy.newProxyInstance( targetInterface.getClassLoader(), new Class<?>[]{targetInterface}, new AdapterInvocationHandler(adaptee, targetInterface) ); } private static class AdapterInvocationHandler implements InvocationHandler { private final Object adaptee; private final Map<Method, Method> methodMapping; AdapterInvocationHandler(Object adaptee, Class<?> targetInterface) { this.adaptee = adaptee; this.methodMapping = buildMethodMapping(targetInterface, adaptee.getClass()); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Method targetMethod = methodMapping.get(method); if (targetMethod == null) { throw new UnsupportedOperationException( "No mapping for: " + method.getName()); } // 参数类型转换 Object[] convertedArgs = convertArguments(args, method.getParameterTypes(), targetMethod.getParameterTypes()); Object result = targetMethod.invoke(adaptee, convertedArgs); // 返回类型转换 return convertResult(result, targetMethod.getReturnType(), method.getReturnType()); } // 辅助方法实现... } } // 使用示例 LegacyPaymentProcessor legacyProcessor = new LegacyPaymentProcessor(); PaymentService modernService = DynamicAdapterFactory.createAdapter( legacyProcessor, PaymentService.class ); // modernService现在表现为PaymentService接口,内部调用legacyProcessor

五、适配器与相关模式的辨析

适配器模式 vs 代理模式:适配器改变接口以解决不兼容问题;代理模式保持相同接口,增加控制访问的间接层。装饰器模式同样保持接口,增加额外职责。

适配器模式 vs 外观模式:适配器针对单一组件的接口转换;外观模式为整个子系统提供简化的统一接口。适配器是"一对一"的转换,外观是"多对一"的封装。

适配器模式 vs 桥接模式:适配器解决已有接口的不兼容;桥接模式在设计阶段分离抽象与实现,使二者独立演化。前者是事后补救,后者是事前规划。

六、现代架构中的适配器思想

API网关:本质上是大量微服务接口的统一适配器,将内部REST/gRPC协议转换为外部GraphQL/REST,处理认证、限流、协议转换等横切关注点。

服务网格(Service Mesh):Sidecar代理作为透明的网络适配器,将服务间通信从业务代码中剥离,实现TLS加密、流量管理、可观测性等能力的无侵入注入。

云原生存储适配器:CSI(Container Storage Interface)、CNI(Container Network Interface)等标准接口,使不同厂商的存储和网络方案能够无缝接入Kubernetes生态。

多租户SaaS的数据隔离适配器:将标准的数据访问接口适配为按租户分库分表、行级安全策略、或独立Schema的不同实现。

七、设计考量与最佳实践

适配器的粒度控制:过细的适配器导致类爆炸,过粗的适配器违背单一职责。建议按"一个被适配者对应一个适配器"的原则,复杂转换可拆解为多个私有方法。

适配器的生命周期管理:有状态适配器需注意线程安全;持有昂贵资源(如数据库连接、网络会话)的适配器应实现Closeable接口,配合依赖注入容器的生命周期回调。

适配器的测试策略:被适配者通常是外部依赖,单元测试时应使用Mock替代;集成测试则需搭建真实的被适配者环境,验证转换逻辑的正确性。

渐进式重构中的适配器定位:适配器是"防腐层"(Anti-Corruption Layer)的核心实现,在DDD战略设计中保护领域模型免受外部污染。随着遗留系统逐步替换,适配器应被标记为技术债务,制定退役计划。

八、结语

适配器模式是软件工程中最务实的设计模式之一。它不追求理想化的统一,而是在承认差异的基础上寻求共存之道。在系统演进的漫漫长河中,接口的变迁永无止境,适配器如同桥梁与翻译,使新旧世界能够对话,使异构系统能够协作。掌握适配器模式,意味着掌握了在复杂现实中寻找最小阻力路径的智慧——这不仅是技术能力,更是一种工程哲学的体现。

返回知识中心