适配器设计模式

2023-12-23 23:29:10

4. 适配器设计模式

4.1 原理与实现

4.1.1 原理分析

适配器设计模式(Adapter Design Pattern)是一种结构型设计模式,用于解决两个不兼容接口之间的问题。适配器允许将一个类的接口转换为客户端期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。

在适配器设计模式中,主要包含以下四个角色:

  • 目标接口(Target):这是客户端期望使用的接口,它定义了特定领域的操作和方法。
  • 需要被适配的类(Adaptee):这是一个已存在的类,它具有客户端需要的功能,但其接口与目标接口不兼容。适配器的目标是使这个类的功能能够通过目标接口使用。
  • 适配器(Adapter):这是适配器模式的核心角色,它实现了目标接口并持有需要适配的类的一个实例。适配器通过封装被适配类Adaptee的功能,使其能够满足Target接口的要求。
  • 客户端(Client):这是使用目标接口的类。客户端与目标接口进行交互,不直接与需要适配的类交互。通过使用适配器,客户端可以间接地使用需要适配的类的功能。

适配器模式的主要目的是:在不修改现有代码的情况下,使不兼容的接口能够协同工作。通过引入适配器角色,客户端可以使用目标接口与需要适配的类进行通信,从而实现解耦和扩展性。

适配器模式有两种实现方式:类适配器对象适配器

4.1.2 类适配器

类适配器使用继承来实现适配器功能。适配器类继承了原有的类(Adaptee)并实现了目标接口(Target)。

// 目标接口
public interface Target {
    void request();
}
// 需要被适配的类
public class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}
// 类适配器,使用继承实现适配功能, 并实现了目标接口target
public class ClassAdapter extends Adaptee implements Target {
    @Override
    public void request() {
        specificRequest();
    }
}

测试案例

public class AdapterPatternTest {
    @Test
    public void testClassAdapter() {
        // 通过调用适配器类的request方法来实现被适配类的specificRequest方法
        Target target = new ClassAdapter();
        target.request();
    }
}    

4.4.3 对象适配器

对象适配器使用组合来实现适配器功能。适配器类包含一个原有被适配类的实例(Adaptee)并实现了目标接口(Target)。

// 目标接口
public interface Target {
    void request();
}
// 需要被适配的类
public class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}
// 对象适配器类,使用组合实现适配功能
public class ObjectAdapter implements Target {
    private Adaptee adaptee;

    public ObjectAdapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

适配器模式可以用于解决不同系统、库或API之间的接口不兼容问题,使得它们可以协同工作。在实际开发中,应根据具体需求选择使用类适配器还是对象适配器。

4.2 使用场景

4.2.1 封装有缺陷的接口设计

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

案例代码:

//这个类来自外部sdk,我们无权修改它的代码
public class Outer { 
	//...
	public static void staticFunction1() { //...
	}
	
	public void uglyNamingFunction2() { //...
	}
	public void tooManyParamsFunction3(int paramA, int paramB, ...) { //...
	}
	public void lowPerformanceFunction4() { //...
	}
}

// 使用适配器模式进行重构
public class ITarget {
	void function1();
	void function2();
	void fucntion3(ParamsWrapperDefinition paramsWrapper);
	void function4();
	//...
}

// 注意:适配器类的命名不一定非得末尾带Adaptor
public class OuterAdaptor extends Outer 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...
	}
}

4.2.2 统一多个类的接口设计

某个功能的实现依赖多个外部系统(或类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后使用多态的特性来复用代码逻辑。

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

// 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) {
		//...
	}
}

未使用适配器模式之前的代码:代码的可测试性、扩展性不好。如果新增其他敏感词API,需要修改RiskManagement类。

public class RiskManagement {
	private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
	private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
	private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
    
	public String filterSensitiveWords(String text) {
        // 过滤A敏感词
		String maskedText = aFilter.filterSexyWords(text);
		maskedText = aFilter.filterPoliticalWords(maskedText);
        // 过滤B敏感词
		maskedText = bFilter.filter(maskedText);
        // 过滤C敏感词
		maskedText = cFilter.filter(maskedText, "***");
		return maskedText;
	}
}

// 测试案例
public class testRiskManagement{
    // 测试1
    @Test
    public void test(){
        RiskManagement risk = new RiskManagement();
        // 过滤敏感词
        String srcText = "sex,fuck,hello world";
        String destText = risk.filterSensitiveWords(srcText);
        System.out.println(destText);
    }
}    

使用适配器模式进行改造

// 统一接口定义
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) {
       return bFilter.filter(text);
    }
}    
// C适配器类
public class CSensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
	private CSensitiveWordsFilter cfilter;
    public String filter(String text) {
       return cfilter.filter(text);
    }
}   

如果添加一个新的敏感词过滤系统,这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好,更加符合开闭原则。

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;
    }
}

// 测试案例
public class testRiskManagement{
    // 测试2
    @Test
    public void test(){
        private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
        private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
        private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
        RiskManagement risk = new RiskManagement();
        risk.addSensitiveWordsFilter(aFilter);
        risk.addSensitiveWordsFilter(bFilter);
        risk.addSensitiveWordsFilter(cFilter);
        // 过滤敏感词
        String srcText = "sex,fuck,hello world";
        String destText = risk.filterSensitiveWords(srcText);
        System.out.println(destText);
    }
}

4.2.3 替换依赖的外部系统

当我把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

// 外部系统A接口
public interface IA {
    //...
    void fa();
}
// 外部系统A实现
public class A implements IA {
    //...
    public void fa() { //...
    }
}

// 外部系统B
public class B {
    //...
    public void fB() { //...
    }
}

在项目Demo中,外部系统A的使用示例

public class Demo {
    private IA a;
    public Demo(IA a) {
    	this.a = a;
    }
	// 使用外部系统A的fa()
    public void fa() {
        this.a.fa();
    }
}

public class DemoTest{
    @Test
    public void test(){
        Demo demo = new Demo(new A());
        demo.fa();
    }
}

使用适配器将外部系统A替换成外部系统B, 调用IA接口的地方都无需改动

public class BAdaptor implemnts IA {
    private B b;
    public BAdaptor(B b) {
    	this.b= b;
    }
    // 重写IA#fa()
    public void fa() {
        // 替换为外部系统B的fb()
        b.fb();
    }
}

只需要将BAdaptor如下注入到Demo即可完成替换。

public class TestBAdaptor{
    @Test
    public void testBAdaptor(){
        Demo demo = new Demo(new BAdaptor(new B()));
        demo.fa();
    }
}

4.2.4 兼容老版本接口

在兼容老版本的角度上思考,一般有两种场景:

  • 兼容老版本接口,新版本接口要在老版本接口做扩展,两个版本均可用;
  • 老版本接口计划废弃,标注deprecated,但是不想改动已有代码,让两个版本兼容并行,但新功能不使用老版本。

场景一

老版本的支付接口

// 老版本支付接口
public interface OldPayment {
	void pay(double amount);
}

新版本的支付接口:

// 新版本支付接口
public interface NewPayment {
	void makePayment(double amount, String currency);
}

创建一个适配器类,实现新版本的支付接口,并在内部使用老版本的支付接口:

// 适配器类,实现新版本支付接口
public class PaymentAdapter implements NewPayment {
    private OldPayment oldPayment;
    public PaymentAdapter(OldPayment oldPayment) {
    	this.oldPayment = oldPayment;
    }
    @Override
    public void makePayment(double amount, String currency) {
        // 假设老版本支付接口只接受人民币,我们需要将其他货币转换为人民币
        if ("CNY".equals(currency)) {
        	oldPayment.pay(amount);
        } else {
        	double convertedAmount = convertToCNY(amount, currency);
            oldPayment.pay(convertedAmount);
		}
	}
    
    private double convertToCNY(double amount, String currency) {
        // 在这里进行货币转换的逻辑
        // 为了简化示例,我们假设所有其他货币都是1:1兑换人民币
    	return amount;
    }
}

最后,在客户端代码中使用适配器类,使其可以兼容新旧两种支付接口:

public class Client {
    public static void main(String[] args) {
        // 创建一个老版本支付实例
        OldPayment oldPaymentInstance = new OldPaymentImpl();
        // 创建适配器实例
        NewPayment paymentAdapter = new PaymentAdapter(oldPaymentInstance);
        // 通过适配器使用新版本支付接口
        paymentAdapter.makePayment(100, "CNY");
        paymentAdapter.makePayment(200, "USD");
    }
}

以上示例,在不修改原有OldPayment 接口的情况下,实现新旧接口的兼容。

接下来看第二种场景,老版本的接口要废弃不使用,但是很多地方使用了老版本的接口,我们想在不影响新老接口的使用的情况下,完成升级。

可以将适配器类修改为实现老版本接口,然后在内部使用新版本接口。这样,原有的代码可以继续使用适配器类,而不需要进行任何修改。

// 老版本支付接口
public interface OldPayment {
	void pay(double amount);
}

// 新版本支付接口
public interface NewPayment {
	void makePayment(double amount, String currency);
}

创建一个适配器类,实现老版本的支付接口,并在内部使用新版本的支付接口:

// 适配器类,实现老版本支付接口
public class PaymentAdapter implements OldPayment {
    private NewPayment newPayment;
    public PaymentAdapter(NewPayment newPayment) {
    	this.newPayment = newPayment;
    }
    @Override
    public void pay(double amount) {
        // 假设新版本支付接口使用人民币,我们直接调用新接口
        newPayment.makePayment(amount, "CNY");
    }
}

在客户端代码中,将原来使用老版本接口的实例替换为适配器实例:

public class Client {
    public static void main(String[] args) {
        // 创建一个新版本支付实例
        NewPayment newPaymentInstance = new NewPaymentImpl();
        // 创建适配器实例(我们只需要将这个新的适配器实例注入容器即可)
        OldPayment paymentAdapter = new PaymentAdapter(newPaymentInstance);
        // 通过适配器使用老版本支付接口, 实际使用的是新接口。老接口可以废弃
        paymentAdapter.pay(100);
    }
}

这样就可以在废弃老版本接口的情况下,实现新旧接口的兼容。原有的代码可以继续使用适配器类,而不需要进行任何修改。

4.3 源码中的应用

4.3.1 日志框架中的应用

学习Java日志

Java 中有很多日志框架,比较常用的有 log4j、logback,以及 JDK 提供的JUL(java.util.logging) 和 Apache 的 JCL(Jakarta Commons Logging)等。

大部分日志框架都提供了相似的功能,比如按照不同级别(debug、info、warn、erro……)打印日志等,但它们却并没有实现统一的接口。这主要是历史的原因,它不像 JDBC 那样,一开始就制定了数据库操作的接口规范。

如果我们只是开发一个自己用的项目,那用什么日志框架都可以,log4j、logback 随便选一个就好。但如果我们开发的是一个集成到其他系统的组件、框架、类库等,那日志框架的选择就没那么随意了。

举一个例子,项目中用到的某个组件使用 log4j 来打印日志,而我们项目本身使用的是 logback。将组件引入到项目之后,我们的项目就相当于有了两套日志打印框架。每种日志框架都有自己特有的配置方式。所以,我们要针对每种日志框架编写不同的配置文件(比如,日志存储的文件地址、打印日志的格式)。如果引入多个组件,每个组件使用的日志框架都不一样,那日志本身的管理工作就变得非常复杂。所以,为了解决这个问题,我们需要统一日志打印框架。

Slf4j 日志框架,它相当于 JDBC 规范,是一套日志门面,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。

Slf4j 的出现晚于 JUL、JCL、log4j 等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合 Slf4j 接口规范。Slf4j 也事先考虑到了这个问题,所以它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框架的接口进行二次封装,适配成统一的 Slf4j 接口定义。

我们接下来就以slf4j为例,看看其中的绑定和桥接功能是如何巧妙实现兼容不同形式的日志的。

使用SLF4J(Simple Logging Facade for Java)绑定Log4j后,就可以无脑使用SLF4J的api进行日志记录,而实现还是原来的log4j实现,为了完成此功能我们需要执行以下步骤:

(1)添加SLF4J和Log4j依赖

<dependencies>
    <!-- 添加SLF4J API依赖 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.32</version>
    </dependency>
    <!-- 桥接包:添加SLF4J绑定Log4j依赖,这个依赖是关键,使用了适配器模式进行了适配 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.32</version>
    </dependency>
    <!-- 添加Log4j依赖 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

(2) 在项目的resources 目录下,创建一个名为log4j.properties 的配置文件,在这里定义日志记录的级别、格式和输出位置

# 这个配置表示根日志级别为INFO,日志将输出到控制台,日志格式为日期 时间 日志级别 类名:行号 - 消息内容。
# 设置Log4j的根日志级别为INFO
log4j.rootLogger=INFO, stdout
# 配置输出到控制台
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss}%-5p %c{1}:%L - %m%n

(3)编写Java代码使用SLF4J API进行日志记录

public class SLF4JExample {
    // 获取Logger实例
    private static final Logger logger = LoggerFactory.getLogger(SLF4JExample.class);
    public static void main(String[] args) {
        // 使用SLF4J API记录不同级别的日志
        // 日志级别优先级 ERROR > WARN > INFO > DEBUG
        logger.debug("这是一条DEBUG级别的日志");
        logger.info("这是一条INFO级别的日志");
        logger.warn("这是一条WARN级别的日志");
        logger.error("这是一条ERROR级别的日志");
        
        // 由于我们的Log4j配置中将根日志级别设置为INFO,所以DEBUG级别的日志不会被输出。
    }
}

(4)运行结果

2023-12-24 12:34:56 INFO SLF4JExample:10 - 这是一条INFO级别的日志
2023-12-24 12:34:56 WARN SLF4JExample:11 - 这是一条WARN级别的日志
2023-12-24 12:34:56 ERROR SLF4JExample:12 - 这是一条ERROR级别的日志

日志框架的桥接包适配原理:

slf4j-log4j12 是一个SLF4J的实现库,它将SLF4J API的日志记录请求转发给Log4j1.2作为底层日志实现框架。它实际上是一个适配器,将SLF4J API与Log4j 1.2 API进行了适配。看一下源码中的关键部分,以理解其实现原理。

(1) Log4jLoggerFactory : slf4j-log4j12 实现了SLF4J的ILoggerFactory接口,创建Log4j 1.2的Logger 实例。这个工厂类负责将SLF4J的请求转换为Log4j1.2的请求。

public class Log4jLoggerFactory implements ILoggerFactory {
    public Logger getLogger(String name) {
        // 实际是Log4j 1.2的Logger实例
        org.apache.log4j.Logger log4jLogger = LogManager.getLogger(name);
        // 将Log4j 1.2的Logger实例包装成SLF4J的Logger实例并返回
        return new Log4jLoggerAdapter(log4jLogger);
    }
}

(2) Log4jLoggerAdapter :这个类实现了SLF4J的Logger 接口,将SLF4J API转换为Log4j 1.2的API。它包装了一个Log4j 1.2的Logger 实例,用于实际的日志记录。

// 适配器类
public final class Log4jLoggerAdapter extends MarkerIgnoringBase {
	final Logger logger; // Log4j 1.2的Logger实例
    public Log4jLoggerAdapter(Logger logger) {
        this.logger = logger;
        this.name = logger.getName();
    }
    public boolean isDebugEnabled() {
        return logger.isDebugEnabled();
    }
    public void debug(String msg) {
    	logger.log(FQCN, Level.DEBUG, msg, null);
    }
    // 其他方法,例如info(), error()等,也类似地转发给Log4j 1.2的Logger实例
}

当在项目中调用SLF4J的LoggerFactory 获取一个Logger 实例时,SLF4J会自动发现并使用slf4j-log4j12 提供的Log4jLoggerFactory ,Log4jLoggerFactory 会创建一个Log4jLoggerAdapter 实例,这个实例内部包装了一个Log4j 1.2的Logger 。当我们使用SLF4J API进行日志记录时,Log4jLoggerAdapter 会将这些请求转换为Log4j 1.2可以处理的请求,从而实现了日志绑定。

通过适配器模式, slf4j-log4j12 实现了SLF4J API与Log4j 1.2的无缝集成,使得可以在项目中使用SLF4J API进行日志记录,同时底层使用Log4j 1.2作为实际的日志框架。这使得客户端代码只需关注SLF4J API,而无需关心底层日志框架的实现细节。此外,这种设计还为我们提供了灵活性,可以轻松地在不同的日志框架之间进行切换,只需更改项目依赖即可。

4.3.2 SpringMVC框架中的应用

在SpringMVC中,使用了适配器设计模式来适配各种类型的处理器(Handler)。例如, org.springframework.web.servlet.HandlerAdapter 接口为各种处理器提供了统一的适配。具体实现类有org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 等。

HandlerAdapter接口

public interface HandlerAdapter {
    boolean supports(Object handler);
    
    ModelAndView handle(HttpServletRequest request, HttpServletResponseresponse, Object handler) 
        throws Exception;
    
    long getLastModified(HttpServletRequest request, Object handler);
}


RequestMappingHandlerAdapter类实现了HandlerAdapter接口

public class RequestMappingHandlerAdapter extends WebContentGenerator implements HandlerAdapter {
    // ...
    // 判断是否支持此处理器
    public boolean supports(Object handler) {
    	return handler instanceof HandlerMethod;
    }
    
    // 处理请求
    public ModelAndView handle(HttpServletRequest request,HttpServletResponse response, Object handler) 	throws Exception {
    	// ...
    }
    
    // 获取最后修改时间
    public long getLastModified(HttpServletRequest request, Object handler) {
    	// ...
    }
}

4.4 代理、桥接、装饰器、适配器的区别

代理、桥接、装饰器、适配器,这 4 种设计模式是比较常用的结构型设计模式。它们的代码结构非常相似,都可以称为 Wrapper 模式,也就是通过Wrapper 类二次封装原始类。尽管代码结构相似,但这 4 种设计模式的用意完全不同,要解决的问题、应用场景不同,这也是它们的主要区别。

这里我就简单说一下它们之间的区别:

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

文章来源:https://blog.csdn.net/u013044713/article/details/135176154
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。