• 适配器模式


    如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

      前面学习了代理、装饰器模式,今天来了解另一个常用的结构型模式:适配器模式。适配器模式也较简单、易理解,应用场景也不会像桥接模式那样让人掌握不透,总而言之就是容易掌握。

    实现方式

      适配器模式,顾名思义,就是用来做适配的,它可以将原本不兼容的接口转换为可兼容的接口,让原本不能够一起工作的类可以一起工作。对于这个模式,比较通俗常见的解释,就是 USB 转接头充当适配器,把两个不兼容的接口,通过接口转变变得可以一起工作。
      适配器模式有两种实现方式:类适配和对象适配,前者通过继承实现,后者通过组合实现。看一组具体的代码实现,其中,ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget接口定义的类,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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

      针对两种实现方式,各自都有各自的特点,总结下来就是:

    1. 如果 Adaptee 的接口并不多,那么两种实现方式都可以。
    2. 如果 Adaptee 的接口很多,并且大部分的方法定义都类似,那么推荐使用类适配器,因为可以直接复用父类的一些逻辑,减少代码量与工作量。
    3. 如果 Adaptee 的接口很多,并且大部分的方法定义都大不相同,那么就使用对象适配器,因为组合结构对于继承更加灵活。

    应用场景总结

      原理与实现都相对简单。那么重点在于我们什么时候去使用它。一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。
      前面提到,适配器模式就是为了处理“接口不兼容”,那么在实际的开发中,什么情况下才会出现接口不兼容呢?你可以自己先思考个几分钟。

    1. 封装有缺陷的接口设计

      假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
      这样说可能无法体现,直接看代码会比较清晰:

    public class CD { //这个类来自外部sdk,我们无权修改它的代码
    	//...
    	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();
    	//...
    }
    
    // 注意:适配器类的命名不一定非得末尾带Adaptor
    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...
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2. 统一多个类的设计

      这个应该是最常见的使用场景。举一个例子,假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码:

    public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
    	//text是原始文本,函数输出用***替换敏感词之后的文本
    	public String filterSexyWords(String text) {
    		// ...
    	}
    	public String filterPoliticalWords(String text) {
    		// ...
    	}
    }
    
    
    public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口
    	public String filter(String text) {
    		//...
    	}
    }
    
    public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
    	public String filter(String text) {
    		//...
    	}
    }
    
    // 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
    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);
    }
    
    public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
    	private ASensitiveWordsFilter aFilter;
    	
    	public String filter(String text) {
    		String maskedText = aFilter.filterSexyWords(text);
    		maskedText = aFilter.filterPoliticalWords(maskedText);
    		return maskedText;
    	}
    }
    
    //...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...
    
    // 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
    // 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好
    
    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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    3. 替换依赖的外部系统

      当我们需要把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动(比如我公司最近终于将数据库连接池从 uupool 换到了 druid,结束了这个老家伙的服役)。

    // 外部系统A
    public interface IA {
    	//...
    	void fa();
    }
    
    public class A implements IA {
    	//...
    	public void fa() { //... }
    }
    
    // 在我们的项目中,外部系统A的使用示例
    public class Demo {
    	private IA a;
    	public Demo(IA a) {
    		this.a = a;
    	}
    	//...
    }
    
    Demo d = new Demo(new A());
    
    // 将外部系统A替换成外部系统B
    public class BAdaptor implemnts IA {
    	private B b;
    	public BAdaptor(B b) {
    		this.b= b;
    	}
    	public void fa() {
    		//...
    		b.fb();
    	}
    }
    
    // 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
    // 只需要将BAdaptor如下注入到Demo即可。
    Demo d = new Demo(new BAdaptor(new B()));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    4. 兼容老版本接口

      这个场景是做项目升级的、重构时常用的。
      比如在做版本升级的时候,需要同时升级下游系统和上游系统,但此时恰巧因为一些特殊原因,下游系统升级完毕,但是上游系统因为失误,可能需要十几分钟来修复,这时候如果你没对升级的接口做兼容,那么这些接口在接下来十几分钟内都是烂的。又或者是需要废弃一些老接口,不直接删除,而是暂时做保留做过渡。
      JDK1.0 中包含一个遍历集合容器的类 Enumeration。JDK2.0 对这个类进行了重构,将它改名为 Iterator 类,并且对它的代码实现做了优化。但是考虑到如果将 Enumeration 直接从 JDK2.0 中删除,那使用 JDK1.0 的项目如果切换到 JDK2.0,代码就会编译不通过。为了避免这种情况的发生,我们必须把项目中所有使用到 Enumeration 的地方,都修改为使用 Iterator 才行。
      单独一个项目做 Enumeration 到 Iterator 的替换,勉强还能接受。但是,使用 Java 开发的项目太多了,一次 JDK 的升级,导致所有的项目不做代码修改就会编译报错,这显然是不合理的。这就是我们经常所说的不兼容升级。为了做到兼容使用低版本 JDK 的老代码,我们可以暂时保留 Enumeration 类,并将其实现替换为直接调用 Itertor。代码示例如下所示:

    public class Collections {
    	public static Emueration emumeration(final Collection c) {
    		return new Enumeration() {
    			Iterator i = c.iterator();
    			public boolean hasMoreElments() {
    				return i.hashNext();
    			} 
    			public Object nextElement() {
    				return i.next():
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    1.2.C++项目:仿mudou库实现并发服务器之时间轮的设计
    云原生周刊:Argo CD v2.12 发布候选版本 | 2024.06.24
    脂质体包裹二硫化钼量子点liposome-MoS2|二硫化钼纳米片/硫化铜纳米粒子|介孔二氧化硅包裹二硫化钼纳米颗粒
    Linux学习笔记——Shell和Bash
    如何设计高性能架构
    Netty02——Netty 入门
    机器学习整理(神经网络)
    二叉树练习
    使用Spark探索数据
    数据结构与算法——排序(C语言实现)
  • 原文地址:https://blog.csdn.net/qq_43654226/article/details/126563065