设计模式-结构型-常用:代理模式、桥接模式、装饰者模式、适配器模式

代理模式

快速入门

代理模式是指在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

比如这段统计性能的代码:

public class UserController {//...省略其他属性和方法...private MetricsCollector metricsCollector; // 依赖注入public UserVo login(String telephone, String password) {long startTimestamp = System.currentTimeMillis();// ... 省略login逻辑...long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);//...返回UserVo数据...}public UserVo register(String telephone, String password) {long startTimestamp = System.currentTimeMillis();// ... 省略register逻辑...long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);//...返回UserVo数据...}
}

在这段代码中,计算性能的代码块侵入到了登录和注册的方法内,跟业务代码高度耦合,一方面未来替换或扩展性很差,另一方面不符合业务类的单一职责要求。

为了解耦,我们可以创建一个接口IUserController,和一个代理类UserControllerProxy,让原始类和代理类都实现这个接口。然后原始类负责业务功能,代理类负责其他附加功能(比如计算性能),然后通过委托的方式调用原始类来执行业务代码。

public interface IUserController {UserVo login(String telephone, String password);UserVo register(String telephone, String password);
}public class UserController implements IUserController {//...省略其他属性和方法...@Overridepublic UserVo login(String telephone, String password) {//...省略login逻辑...//...返回UserVo数据...}@Overridepublic UserVo register(String telephone, String password) {//...省略register逻辑...//...返回UserVo数据...}
}public class UserControllerProxy implements IUserController {private MetricsCollector metricsCollector;private UserController userController;public UserControllerProxy(UserController userController) {this.userController = userController;this.metricsCollector = new MetricsCollector();}@Overridepublic UserVo login(String telephone, String password) {long startTimestamp = System.currentTimeMillis();// 委托UserVo userVo = userController.login(telephone, password);long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);return userVo;}@Overridepublic UserVo register(String telephone, String password) {long startTimestamp = System.currentTimeMillis();UserVo userVo = userController.register(telephone, password);long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);return userVo;}
}//UserControllerProxy使用举例
//因为原始类和代理类实现相同的接口,是基于接口而非实现编程
//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController());

不过上面的实现还有点问题,如果原始类不是我们维护的,没有办法修改,就不能给它新定义一个接口,这种情况一般是采用继承的方式,让代理类UserControllerProxy继承原始类UserController。

public class UserControllerProxy extends UserController {private MetricsCollector metricsCollector;public UserControllerProxy() {this.metricsCollector = new MetricsCollector();}public UserVo login(String telephone, String password) {long startTimestamp = System.currentTimeMillis();UserVo userVo = super.login(telephone, password);long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);return userVo;}public UserVo register(String telephone, String password) {long startTimestamp = System.currentTimeMillis();UserVo userVo = super.register(telephone, password);long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);return userVo;}
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

动态代理

不过,刚刚的代码实现还是有点问题。一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。

如果有50个要添加附加功能的原始类,那我们就要创建50个对应的代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。那这个问题怎么解决呢?

我们可以使用动态代理来解决这个问题。所谓动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。那如何实现动态代理呢?

实际上,动态代理底层依赖的就是Java的反射语法,我们来看一下,如何用Java的动态代理来实现刚刚的功能。具体的代码如下所示。其中,MetricsCollectorProxy作为一个动态代理类,动态地给每个需要收集接口请求信息的类创建代理类。

public class MetricsCollectorProxy {private MetricsCollector metricsCollector;public MetricsCollectorProxy() {this.metricsCollector = new MetricsCollector();}public Object createProxy(Object proxiedObject) {Class[] interfaces = proxiedObject.getClass().getInterfaces();DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);}private class DynamicProxyHandler implements InvocationHandler {private Object proxiedObject;public DynamicProxyHandler(Object proxiedObject) {this.proxiedObject = proxiedObject;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {long startTimestamp = System.currentTimeMillis();Object result = method.invoke(proxiedObject, args);long endTimeStamp = System.currentTimeMillis();long responseTime = endTimeStamp - startTimestamp;String apiName = proxiedObject.getClass().getName() + ":" + method.getName();RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);metricsCollector.recordRequest(requestInfo);return result;}}
}//MetricsCollectorProxy使用举例
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());

实际上,Spring AOP底层的实现原理就是基于动态代理。用户配置好需要给哪些类创建代理,并定义好在执行原始类的业务代码前后执行哪些附加功能。Spring为这些类创建动态代理对象,并在JVM中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法,也就实现了给原始类添加附加功能的目的。

应用场景

代理模式的应用场景非常多,我这里列举一些比较常见的用法,希望你能举一反三地应用在你的项目开发中。

业务系统的非功能性需求开发
代理模式最常用的一个应用场景就是,在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。实际上,前面举的搜集接口请求信息的例子,就是这个应用场景的一个典型例子。

如果你熟悉Java语言和Spring开发框架,这部分工作都是可以在Spring AOP切面中完成的。前面我们也提到,Spring AOP底层的实现原理就是基于动态代理。

代理模式在RPC、缓存中的应用
实际上,RPC框架也可以看作一种代理模式,GoF的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用RPC服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。

关于远程代理的代码示例,我自己实现了一个简单的RPC框架Demo,放到了GitHub中,你可以点击这里的链接查看。

我们再来看代理模式在缓存中的应用。假设我们要开发一个接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理。比如,针对获取用户个人信息的需求,我们可以开发两个接口,一个支持缓存,一个支持实时查询。对于需要实时数据的需求,我们让其调用实时查询接口,对于不需要实时数据的需求,我们让其调用支持缓存的接口。那如何来实现接口请求的缓存功能呢?

最简单的实现方法就是刚刚我们讲到的,给每个需要支持缓存的查询需求都开发两个不同的接口,一个支持缓存,一个支持实时查询。但是,这样做显然增加了开发成本,而且会让代码看起来非常臃肿(接口个数成倍增加),也不方便缓存接口的集中管理(增加、删除缓存接口)、集中配置(比如配置每个接口缓存过期时间)。

针对这些问题,代理模式就能派上用场了,确切地说,应该是动态代理。如果是基于Spring框架来开发的话,那就可以在AOP切面中完成接口缓存的功能。在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在AOP切面中拦截请求,如果请求中带有支持缓存的字段(比如http://…?..&cached=true),我们便从缓存(内存缓存或者Redis缓存等)中获取数据直接返回。

扩展-AOP

Spring AOP的实现原理也是基于动态代理,可以通过实现InvocationHandler接口,在构造函数中注入被代理的类对象,然后重写invoke方法调用被代理的方法。关于InvocationHandler接口的原理,如果深究可以阅读proxyFactoryBean的源码,若是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;若是采用CGLIB代理,则是创建ObjenesisCglibAopProxy,前者的逻辑就和上述的例子差不多。

另外,使用Spring AOP时,需要保证我们始终与代理对象交互,而不是其本身,比如下面这个类中的foo()方法调用了bar(),哪怕Spring AOP对bar()做了拦截,由于调用的不是代理对象,因而看不到任何效果。

public class Hello {public void foo() {bar();}public void bar() {...}
}

桥接模式

这玩意真的是常用的?。。。暂时先把搞懂的部分发上来

概念

在GoF的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”

关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于“组合优于继承”设计原则。

实现举例

比如一个API接口监控告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员。

我们先来看最简单、最直接的一种实现方式。代码如下所示:

public enum NotificationEmergencyLevel {SEVERE, URGENCY, NORMAL, TRIVIAL
}public class Notification {private List emailAddresses;private List telephones;private List wechatIds;public Notification() {}public void setEmailAddress(List emailAddress) {this.emailAddresses = emailAddress;}public void setTelephones(List telephones) {this.telephones = telephones;}public void setWechatIds(List wechatIds) {this.wechatIds = wechatIds;}public void notify(NotificationEmergencyLevel level, String message) {if (level.equals(NotificationEmergencyLevel.SEVERE)) {//...自动语音电话} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {//...发微信} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {//...发邮件} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {//...发邮件}}
}//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {public ErrorAlertHandler(AlertRule rule, Notification notification){super(rule, notification);}@Overridepublic void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}
}

Notification类的代码实现有一个最明显的问题,那就是有很多if-else分支逻辑。实际上,如果每个分支中的代码都不复杂,后期也没有无限膨胀的可能(增加更多if-else分支判断),那这样的设计问题并不大,没必要非得一定要摒弃if-else分支逻辑。

不过,Notification的代码显然不符合这个条件。因为每个if-else分支中的代码逻辑都比较复杂,发送通知的所有逻辑都扎堆在Notification类中。我们知道,类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组装在一起。

针对Notification的代码,我们将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender相关类)。其中,Notification类相当于抽象,MsgSender类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,我们可以动态地去指定(比如,通过读取配置来获取对应关系)。

按照这个设计思路,我们对代码进行重构。重构之后的代码如下所示:

public interface MsgSender {void send(String message);
}public class TelephoneMsgSender implements MsgSender {private List telephones;public TelephoneMsgSender(List telephones) {this.telephones = telephones;}@Overridepublic void send(String message) {//...}}public class EmailMsgSender implements MsgSender {// 与TelephoneMsgSender代码结构类似,所以省略...
}public class WechatMsgSender implements MsgSender {// 与TelephoneMsgSender代码结构类似,所以省略...
}public abstract class Notification {protected MsgSender msgSender;public Notification(MsgSender msgSender) {this.msgSender = msgSender;}public abstract void notify(String message);
}public class SevereNotification extends Notification {public SevereNotification(MsgSender msgSender) {super(msgSender);}@Overridepublic void notify(String message) {msgSender.send(message);}
}public class UrgencyNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}

装饰器模式

概念

速通版:装饰器模式在实现上和代理模式类似,代理模式是用于给业务方法附加其他能力,装饰器模式一般用于增强原始类本身的能力。

下面这样一段代码,我们打开文件test.txt,从中读取数据。其中,InputStream是一个抽象类,FileInputStream是专门用来读取文件流的子类。BufferedInputStream是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

初看上面的代码,我们会觉得Java IO的用法比较麻烦,需要先创建一个FileInputStream对象,然后再传递给BufferedInputStream对象来使用。我在想,Java IO为什么不设计一个继承FileInputStream并且支持缓存的BufferedFileInputStream类呢?这样我们就可以像下面的代码中这样,直接创建一个BufferedFileInputStream类对象,打开文件读取数据,用起来岂不是更加简单?

InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

基于继承的设计方案
如果InputStream只有一个子类FileInputStream的话,那我们在FileInputStream基础之上,再设计一个孙子类BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。但实际上,继承InputStream的子类有很多。我们需要给每一个InputStream的子类,再继续派生支持缓存读取的子类。

除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的DataInputStream类,支持按照基本数据类型(int、boolean、long等)来读取数据。

FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();

在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出DataFileInputStream、DataPipedInputStream等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出BufferedDataFileInputStream、BufferedDataPipedInputStream等n多类。这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。这也是为什么不推荐使用继承。

实现举例

有一个概念是“组合优于继承”,可以“使用组合来替代继承”。针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了Java IO的这种设计思路。不过,我对代码做了简化,只抽象出了必要的代码结构,如果你感兴趣的话,可以直接去查看JDK源码。

public abstract class InputStream {//...public int read(byte b[]) throws IOException {return read(b, 0, b.length);}public int read(byte b[], int off, int len) throws IOException {//...}public long skip(long n) throws IOException {//...}public int available() throws IOException {return 0;}public void close() throws IOException {}public synchronized void mark(int readlimit) {}public synchronized void reset() throws IOException {throw new IOException("mark/reset not supported");}public boolean markSupported() {return false;}
}public class BufferedInputStream extends InputStream {protected volatile InputStream in;protected BufferedInputStream(InputStream in) {this.in = in;}//...实现基于缓存的读数据接口...  
}public class DataInputStream extends InputStream {protected volatile InputStream in;protected DataInputStream(InputStream in) {this.in = in;}//...实现读取基本类型数据的接口
}


看了上面的代码,你可能会问,那装饰器模式就是简单的“用组合替代继承”吗?当然不是。从Java IO的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。

第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。比如,下面这样一段代码,我们对FileInputStream嵌套了两个装饰器类:BufferedInputStream和DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();

第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {void f();
}
public class A impelements IA {public void f() { //... }
}
public class AProxy impements IA {private IA a;public AProxy(IA a) {this.a = a;}public void f() {// 新添加的代理逻辑a.f();// 新添加的代理逻辑}
}// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {void f();
}
public class A impelements IA {public void f() { //... }
}
public class ADecorator impements IA {private IA a;public ADecorator(IA a) {this.a = a;}public void f() {// 功能增强代码a.f();// 功能增强代码}
}

实际上,DataInputStream也存在跟BufferedInputStream同样的问题。为了避免代码重复,Java IO抽象出了一个装饰器父类FilterInputStream,代码实现如下所示。InputStream的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。

public class FilterInputStream extends InputStream {protected volatile InputStream in;protected FilterInputStream(InputStream in) {this.in = in;}public int read() throws IOException {return in.read();}public int read(byte b[]) throws IOException {return read(b, 0, b.length);}public int read(byte b[], int off, int len) throws IOException {return in.read(b, off, len);}public long skip(long n) throws IOException {return in.skip(n);}public int available() throws IOException {return in.available();}public void close() throws IOException {in.close();}public synchronized void mark(int readlimit) {in.mark(readlimit);}public synchronized void reset() throws IOException {in.reset();}public boolean markSupported() {return in.markSupported();}
}

适配器模式

原理和实现

适配器模式的概念就是降不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,举个例子就是USB转接器就是适配器。

适配器模式有两种实现方式:类适配器和对象适配器。其中类适配器用继承关系实现,对象适配器用组合关系来实现。示例如下:

// 类适配器: 基于继承
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();}
}

 针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?判断的标准主要有两个,一个是Adaptee接口的个数,另一个是Adaptee和ITarget的契合程度。

  • 如果Adaptee接口并不多,那两种实现方式都可以。
  • 如果Adaptee接口很多,而且Adaptee和ITarget接口定义大部分都相同,那我们推荐使用类适配器,因为Adaptor复用父类Adaptee的接口,比起对象适配器的实现方式,Adaptor的代码量要少一些。
  • 如果Adaptee接口很多,而且Adaptee和ITarget接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

应用场景

原理和实现讲完了,都不复杂。我们再来看,到底什么时候会用到适配器模式呢?

一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

前面我们反复提到,适配器模式的应用场景是“接口不兼容”。那在实际的开发中,什么情况下才会出现接口不兼容呢?我建议你先自己思考一下这个问题,然后再来看我下面的总结 。

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

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

具体我还是举个例子来解释一下,你直接看代码应该会更清晰。具体代码如下所示:

public class CD { //这个类来自外部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 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...}
}

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

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

// 外部系统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()));

4.兼容老版本接口
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景。同样,我还是通过一个例子,来进一步解释一下。

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():}}}
}

5.适配不同格式的数据
前面我们讲到,适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java中的Arrays.asList()也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。

List stooges = Arrays.asList("Larry", "Moe", "Curly");

扩展-日志中的应用

Java中有很多日志框架,在项目开发中,我们常常用它们来打印日志信息。其中,比较常用的有log4j、logback,以及JDK提供的JUL(java.util.logging)和Apache的JCL(Jakarta Commons Logging)等。

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

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

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

如果你是做Java开发的,那Slf4j这个日志框架你肯定不陌生,它相当于JDBC规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。

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

// slf4j统一的接口定义
package org.slf4j;
public interface Logger {public boolean isTraceEnabled();public void trace(String msg);public void trace(String format, Object arg);public void trace(String format, Object arg1, Object arg2);public void trace(String format, Object[] argArray);public void trace(String msg, Throwable t);public boolean isDebugEnabled();public void debug(String msg);public void debug(String format, Object arg);public void debug(String format, Object arg1, Object arg2)public void debug(String format, Object[] argArray)public void debug(String msg, Throwable t);//...省略info、warn、error等一堆接口
}// log4j日志框架的适配器
// Log4jLoggerAdapter实现了LocationAwareLogger接口,
// 其中LocationAwareLogger继承自Logger接口,
// 也就相当于Log4jLoggerAdapter实现了Logger接口。
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBaseimplements LocationAwareLogger, Serializable {final transient org.apache.log4j.Logger logger; // log4jpublic boolean isDebugEnabled() {return logger.isDebugEnabled();}public void debug(String msg) {logger.log(FQCN, Level.DEBUG, msg, null);}public void debug(String format, Object arg) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.format(format, arg);logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());}}public void debug(String format, Object arg1, Object arg2) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());}}public void debug(String format, Object[] argArray) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());}}public void debug(String msg, Throwable t) {logger.log(FQCN, Level.DEBUG, msg, t);}//...省略一堆接口的实现...
}

所以,在开发业务系统或者开发框架、组件的时候,我们统一使用Slf4j提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback……),是可以动态地指定的(使用Java的SPI技术,这里我不多解释,你自行研究吧),只需要将相应的SDK导入到项目中即可。

不过,你可能会说,如果一些老的项目没有使用Slf4j,而是直接使用比如JCL来打印日志,那如果想要替换成其他日志框架,比如log4j,该怎么办呢?实际上,Slf4j不仅仅提供了从其他日志框架到Slf4j的适配器,还提供了反向适配器,也就是从Slf4j到其他日志框架的适配。我们可以先将JCL切换为Slf4j,然后再将Slf4j切换为log4j。经过两次适配器的转换,我们就能成功将log4j切换为了logback。

结构型模式总结

代理、桥接、装饰器、适配器,这4种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为Wrapper模式,也就是通过Wrapper类二次封装原始类。

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1555990.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

常见排序详解(历时四天,哭了,必须释放一下)

目录 1、插入排序 1.1 基本思想 1.2 直接插入排序 1.2.1 思路 1.2.2 代码实现 1.2.3 性质 1.3 希尔排序 1.3.1 思路 1.3.2 代码实践 1.3.3 性质 2、选择排序 2.1 基本思想 2.2 直接选择排序 2.2.1 思路 2.2.2 代码实践 2.2.3 性质 2.3 堆排序 2.3.1 思路 2.…

108页PPT丨OGSM战略规划框架:实现企业目标的系统化方法论

OGSM战略规划框架是一种实现企业目标的系统化方法论&#xff0c;它通过将组织的目标&#xff08;Objectives&#xff09;、目标&#xff08;Goals&#xff09;、策略&#xff08;Strategies&#xff09;和衡量指标&#xff08;Measures&#xff09;进行系统化整合&#xff0c;确…

windows下DockerDesktop命令行方式指定目录安装

windows下DockerDesktop指定目录安装(重新安装) 因为DcokerDesktop占用内存较大, 并且拉去镜像后占用本地空间较多,所以建议安装时就更改默认安装路径和镜像存储路径 这里,展示了从下载到安装的过程: 首先下载DcokerDesktop;找到Docker Desktop Installer.exe 并重命名为 do…

国内超声波清洗机哪个品牌好?力荐四款超耐用超声波清洗机!

超声波清洗机作为一款高效实用的家庭与专业清洁利器&#xff0c;能够迅速且彻底地清洁多样化的物件。面对市场上琳琅满目的品牌与型号&#xff0c;每一款都各具特色与优势&#xff0c;故在决定购买前做足调研显得尤为重要&#xff0c;以免购入不尽如人意的产品&#xff0c;造成…

力扣110:判断二叉树是否为平衡二叉树

利用二叉树遍历的思想编写一个判断二叉树&#xff0c;是否为平衡二叉树 示例 &#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true思想&#xff1a; 代码&#xff1a; int getDepth(struct TreeNode* node) {//如果结点不存在&#xff0c;返回…

打造自己的RAG解析大模型:Windows部署OCR服务(可商业应用)

在上一篇文章中&#xff0c;我们介绍了如何在 Windows 环境中配置 OCR 相关模型&#xff0c;并完成了模型验证。本篇文章将基于之前的内容&#xff0c;进一步讲解如何将文本检测、方向分类和文本识别模型进行串联&#xff0c;最终搭建一个基础的 OCR 应用服务。通过这些模型的串…

【Diffusion分割】CTS:基于一致性的医学图像分割模型

CTS: A Consistency-Based Medical Image Segmentation Model 摘要&#xff1a; 在医学图像分割任务中&#xff0c;扩散模型已显示出巨大的潜力。然而&#xff0c;主流的扩散模型存在采样次数多、预测结果慢等缺点。最近&#xff0c;作为独立生成网络的一致性模型解决了这一问…

回调函数是什么

回调函数是什么 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另⼀个函数&#xff0c;当这个指针被⽤来调⽤其所指向的函数时&#xff0c;被调⽤的函数就是回调函数。 回调函数不是由该函数的实现⽅直接调⽤&…

Linux驱动开发(速记版)--GPIO子系统

第105章 GPIO 入门 105.1 GPIO 引脚分布 RK3568 有 5 组 GPIO&#xff1a;GPIO0 到 GPIO4。 每组 GPIO 又以 A0 到 A7&#xff0c;B0 到 B7&#xff0c;C0 到C7&#xff0c;D0 到 D7&#xff0c;作为区分的编号。 所以 RK3568 上的 GPIO 是不是应该有 5*4*8160 个呢&#xff1…

Semantic Communication Meets Edge Intelligence——构造终端共享的知识图谱指导无线物联网通信中文本的传输

论文链接&#xff1a; IEEE Xplore Full-Text PDF:https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber9979702 1. 背景 随着自动驾驶、智能城市等应用的发展&#xff0c;移动数据流量将大幅增加。传统的香农信息论&#xff08;CIT&#xff09;通信系统已接近其带…

SpringBoot MyBatis连接数据库设置了encoding=utf-8还是不能用中文来查询

properties的MySQL连接时已经指定了字符编码格式&#xff1a; url: jdbc:mysql://localhost:3306/sky_take_out?useUnicodetrue&characterEncodingutf-8使用MyBatis查询&#xff0c;带有中文参数&#xff0c;查询出的内容为空。 执行的语句为&#xff1a; <select id&…

decltype推导规则

decltype推导规则 当用decltype(e)来获取类型时&#xff0c;编译器将依序判断以下四规则&#xff1a; 1.如果e是一个没有带括号的标记符表达式(id-expression)或者类成员访问表达式&#xff0c;那么decltype(e)就是e所命名的实体的类型。此外&#xff0c;如果e是一个被重载的函…

k8s 中存储之 NFS 卷

目录 1 NFS 卷的介绍 2 NFS 卷的实践操作 2.1 部署一台 NFS 共享主机 2.2 在所有k8s节点中安装nfs-utils 2.3 部署nfs卷 2.3.1 生成 pod 清单文件 2.3.2 修改 pod 清单文件增加 实现 NFS卷 挂载的 参数 2.3.3 声明签单文件并查看是否创建成功 2.3.4 在 NFS 服务器 创建默认发布…

思维导图工具,轻松搞定复杂问题!

一提到思维导图&#xff0c;想必大家都不会陌生&#xff1b;它能帮助我们更好地梳理思路&#xff0c;让复杂的想法变得清晰可见&#xff1b;而随着互联网的普及&#xff0c;在线思维导图工具更是成为了我们日常工作和学习的得力助手&#xff1b;今天&#xff0c;我就来给大家推…

小红书算法岗面试,竞争太激烈了

最近已有不少大厂都在秋招宣讲了&#xff0c;也有一些在 Offer 发放阶段。 节前&#xff0c;我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行了…

【Kubernetes】常见面试题汇总(五十八)

目录 127.创建 PV 失败&#xff1f; 128. pod 无法挂载 PVC&#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#xff09;” 。 题目 69-113 属于【Kubernetes】…

一个月学会Java 第4天 运算符和数据转换

Day4 运算符和数据转换 今天来讲运算符&#xff0c;每个运算符的作用和现象&#xff0c;首先我们先复习一下数据类型&#xff0c; day2讲过基本数据类型有八种&#xff0c;int、short、long、byte、char、boolean、float、double&#xff0c;分别为四个整型、一个字符型、一个布…

基于springboot vue3 在线考试系统设计与实现 源码数据库 文档

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作☆☆☆ 精彩专栏推荐订阅☆☆☆☆…

电脑断网或者经常断网怎么办?

1、首先&#xff0c;按一下键盘的win R &#xff0c; 在打开的运行框内输入&#xff1a;cmd 然后按一下回车 或者 点击一下【确定】 2、在命令窗口输入&#xff1a;ipconfig/release , 然后按一下回车 作用&#xff1a;IP释放&#xff0c;相当于把网线拔了重新插上 3、接着…

佳能基于SPAD的监控摄像机MS-500入选《时代》2023最佳发明

一、产品概述 佳能MS-500是一款采用SPAD(Single Photon Avalanche Diode,单光子雪崩二极管)传感器的监控摄像机。SPAD传感器以其极高的灵敏度和在低光环境下的卓越表现而闻名,使得MS-500能够在夜晚或极暗光条件下拍摄到清晰、彩色的画面。此外,MS-500还配置了高性能的镜头…