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 中有很多日志框架,比较常用的有 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 种设计模式的用意完全不同,要解决的问题、应用场景不同,这也是它们的主要区别。
这里我就简单说一下它们之间的区别:
-
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
-
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
-
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
-
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。