适配器设计模式(Adapter Design Pattern)是一种结构型设计模式,用于解决两个不兼容接口之间的问题。适配器允许将一个类的接口转换为客户端期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
在适配器设计模式中,主要包含以下四个角色:
- 目标接口(Target):这是客户端期望使用的接口,它定义了特定领域的操作和方法。
- 需要适配的类(Adaptee):这是一个已存在的类,它具有客户端需要的功能,但其接口与目标接口不兼容。适配器的目标是使这个类的功能能够通过目标接口使用。
- 适配器(Adapter):这是适配器模式的核心角色,它实现了目标接口并持有需要适配的类的一个实例。适配器通过封装Adaptee的功能,使其能够满足Target接口的要求。
- 客户端(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)Log4jLoggerFactory
:slf4j-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
提供的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,而无需关心底层日志框架的实现细节。此外,这种设计还为提供了灵活性,可以轻松地在不同的日志框架之间进行切换,只需更改项目依赖即可。
通过使用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 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。
**代理模式:**代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
**桥接模式:**桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
**装饰器模式:**装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
**适配器模式:**适配器模式是一种事后的“补救方法”策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。