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

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

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

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

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

类适配器

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

// 目标接口
interface Target {
    void request();
}

// 需要适配的类(Adaptee)
class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}

// 类适配器
class ClassAdapter extends Adaptee implements Target {
    @Override
    public void request() {
        specificRequest();
    }
}

public class ClassAdapterExample {
    public static void main(String[] args) {
        Target target = new ClassAdapter();
        target.request();
    }
}

对象适配器

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

// 目标接口
interface Target {
    void request();
}

// 需要适配的类(Adaptee)
class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}

// 对象适配器
class ObjectAdapter implements Target {
    private Adaptee adaptee;

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

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

public class ObjectAdapterExample {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target target = new ObjectAdapter(adaptee);
        target.request();
    }
}

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

封装有缺陷的接口设计

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

例子代码如下所示:

public class Outer { //这个类来自外部sdk,我们无权修改它的代码
    //...
    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 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...
    }
}

统一多个类的接口设计

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

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

可以配合着下面的代码示例,来理解这个例子。

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

替换依赖的外部系统

把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。具体的代码示例如下所示:

// 外部系统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、老版本接口计划废弃,标注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);
    }
}

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

源码中的使用

1、日志中的应用

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依赖

首先,需要在项目中添加SLF4J和Log4j的依赖。如果使用Maven,可以在pom.xml文件中添加以下依赖:

<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) 创建一个Log4j配置文件

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

例如可以创建如下配置:

propertiesCopy code
# 设置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

这个配置表示根日志级别为INFO,日志将输出到控制台,日志格式为日期 时间 日志级别 类名:行号 - 消息内容

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

编写一个简单的Java类,使用SLF4J API进行日志记录:

public class SLF4JExample {
    // 获取Logger实例
    private static final Logger logger = LoggerFactory.getLogger(SLF4JExample.class);

    public static void main(String[] args) {
        // 使用SLF4J API记录不同级别的日志
        logger.debug("这是一条DEBUG级别的日志");
        logger.info("这是一条INFO级别的日志");
        logger.warn("这是一条WARN级别的日志");
        logger.error("这是一条ERROR级别的日志");
    }
}

(4)运行代码

现在已经完成了SLF4J绑定Log4j的配置和代码编写,接下来可以编译和运行这个示例。

运行结果应该显示如下:

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

请注意,由于Log4j配置中将根日志级别设置为INFO,所以DEBUG级别的日志不会被输出。

至此已经成功地使用SLF4J绑定了Log4j,并通过SLF4J API进行了日志记录。

原理:

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

(1)Log4jLoggerFactoryslf4j-log4j12实现了SLF4J的ILoggerFactory接口,创建Log4j 1.2的Logger实例。这个工厂类负责将SLF4J的请求转换为Log4j 1.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提供的Log4jLoggerFactoryLog4jLoggerFactory会创建一个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,而无需关心底层日志框架的实现细节。此外,这种设计还为提供了灵活性,可以轻松地在不同的日志框架之间进行切换,只需更改项目依赖即可。

通过使用slf4j-log4j12,可以在不修改客户端代码的情况下,将项目中的日志记录从其他日志框架迁移到Log4j 1.2,或者从Log4j 1.2迁移到其他框架。这大大简化了项目中日志框架迁移和升级的工作。

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, HttpServletResponse response, 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 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。

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

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

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

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