【适配器】设计模式:旧系统迁移与第三方库集成的解决方案
引言
适配器设计模式是一种结构设计模式,它允许将一个类的接口转换成客户端期望的另一个接口。适配器让那些接口不兼容的类可以一起工作。这种模式在系统集成、插件开发和第三方库集成中尤为重要。
核心组件:
- 目标接口(ITarget):期望的接口,要转化成的接口定义,客户端将使用这个接口;
- 适配者(Adaptee):已存在的类,具有不兼容
Target
定义的接口; - 适配器(Adaptor):将适配者
Adaptee
的接口转换成目标ITarget
的接口。
实现方式
类适配器
类适配器基于继承实现
// 目标接口
public interface ITarget {
void f1();
void f2();
void fc();
}
// 适配者
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
//适配器
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
//fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
对象适配器
对象适配器基于组合实现
// 目标接口
public interface ITarget {
void f1();
void f2();
void fc();
}
//适配者
public class Adaptee {
public void fa() { ... }
public void fb() { ... }
public void fc() { ... }
}
//适配器
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
方案选择
类适配器和对象适配器在实际工作中该怎么选择呢?有以下两个维度
Adaptee
接口的个数Adaptee
和ITarget
接口的契合程度
1)若Adaptee
接口不多,则两种方式都可以;
2)若Adaptee
接口很多,且Adaptee
和ITarget
接口定义大部分都相同,推荐使用类适配器,因为Adaptor
复用父类Adaptee
的接口,比起对象适配器的实现方式,Adaptor
的代码量要少一些;
3)若Adaptee
接口很多,且Adaptee
和ITarget
接口定义大部分都不相同,推荐使用对象适配器,因为组合结构相对于继承更加灵活。
应用场景
适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷,属于无奈之举。如果在设计初期,就能协调规避接口不兼容的问题,那么这种模式就没有应用的机会了。
那么在实际开发中,什么情况下会出现接口不兼容呢?
封装有缺陷的接口设计
假如我们依赖的外部系统在接口设计上有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计。
//假设这个类来自外部sdk,我们无权修改它的代码
public class CD {
public static void staticFunction1() { //... }
public void uglyNamingFunction2() { //... }
public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
public void lowPerformanceFunction4() { //... }
}
// 使用适配器模式进行重构
// 目标接口
public interface ITarget {
void function1();
void function2();
void fucntion3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
//适配器
public class CDAdaptor extends CD implements ITarget {
public void function1() {
super.staticFunction1();
}
public void function2() {
super.uglyNamingFucntion2();
}
public void function3(ParamsWrapperDefinition paramsWrapper) {
super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
}
public void function4() {
//...reimplement it...
}
}
统一多个类的接口设计
某个功能的实现依赖多个外部系统或者多个类,通过适配器模式,将它们的接口适配为统一的接口定义,然后就可以使用多态特性来复用代码逻辑。
假如系统 A 要对用户输入的文本进行敏感词过滤,为了提高过滤的召回率,引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。
但是,每个系统提供的过滤接口都是不同的。意味着我们无法复用一套逻辑来调用各个系统。此时,我们可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。
未使用适配器模式
// A敏感词过滤系统提供的接口
public class ASensitiveWordsFilter {
//text是原始文本,函数输出用***替换敏感词之后的文本
public String filterSexyWords(String text) {
// ...
}
public String filterPoliticalWords(String text) {
// ...
}
}
// B敏感词过滤系统提供的接口
public class BSensitiveWordsFilter {
public String filter(String text) {
//...
}
}
// C敏感词过滤系统提供的接口
public class CSensitiveWordsFilter {
public String filter(String text, String mask) {
//...
}
}
// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
public String filterSensitiveWords(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
maskedText = bFilter.filter(maskedText);
maskedText = cFilter.filter(maskedText, "***");
return maskedText;
}
}
适配器模式改造
// 统一接口定义
public interface ISensitiveWordsFilter {
String filter(String text);
}
// A 适配器
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private ASensitiveWordsFilter aFilter;
public String filter(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
return maskedText;
}
}
// B 适配器
public class BSensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private BSensitiveWordsFilter bFilter;
public String filter(String text) {
String maskedText = bFilter.filter(text);
return maskedText;
}
}
// C 适配器
public class CSensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private CSensitiveWordsFilter cFilter;
public String filter(String text) {
String maskedText = cFilter.filter(text,"");
return maskedText;
}
}
// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
private List<ISensitiveWordsFilter> filters = new ArrayList<>();
public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
filters.add(filter);
}
public String filterSensitiveWords(String text) {
String maskedText = text;
for (ISensitiveWordsFilter filter : filters) {
maskedText = filter.filter(maskedText);
}
return maskedText;
}
}
替换依赖的外部系统
当我们把项目中依赖的一个外部系统替换为另一个外部系统时,利用适配器模式,可以减少对代码的改动。
假设我们有一个外部系统 A 的接口和实现。
public interface IA {
void fa();
}
public class A implements IA {
public void fa() {
System.out.println("Implementation of fa in System A");
}
}
还有一个使用外部系统 A 的 Demo 类
public class Demo {
private IA a;
public Demo(IA a) {
this.a = a;
}
public void doSomething() {
a.fa();
}
}
现在,我们引入外部系统B
以及适配器BAdaptor
public class B {
public void fb() {
System.out.println("Implementation of fb in System B");
}
}
// B的适配器
public class BAdaptor implements IA {
private B b;
public BAdaptor(B b) {
this.b = b;
}
public void fa() {
// 适配B的fb方法
b.fb();
}
}
最后,我们可以在Demo
中无缝替换A
为B
,而不影响Demo
的代码
public class Client {
public static void main(String[] args) {
// 使用外部系统A
Demo demoWithA = new Demo(new A());
demoWithA.doSomething();
// 替换为外部系统B,借助适配器
Demo demoWithB = new Demo(new BAdaptor(new B()));
demoWithB.doSomething();
}
}
通过引入适配器,我们在替换外部系统时只需修改实例的创建,而不需要修改Demo
中的代码。这符合适配器模式的核心思想,实现了对项目代码的最小侵入性。
适配不同格式的数据
Java 中的 Arrays.asList()
也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。
List<String> stooges = Arrays.asList("A", "B", "C");
总结
适配器设计模式是一种强大的工具,它允许不兼容的接口协同工作。通过创建适配器,我们可以将现有的类集成到新系统中,而无需修改它们的代码。这种模式提高了代码的可重用性和系统的灵活性。
参考资料:
《极客时间-设计模式之美》