当前位置: 首页 > 科技观察

项目升级后如何兼容老界面?尝试适配器模式!

时间:2023-03-23 11:08:55 科技观察

前言AdapterPattern的英文译名是AdapterDesignPattern。顾名思义,这种模式就是用来适配的。它将不兼容的接口转换为兼容的接口,使得因接口不兼容而无法协同工作的类可以协同工作。对于这种模式,有一个经常被用来解释的例子,就是USB适配器作为一个适配器,两个不兼容的接口可以通过适配器协同工作。适配器模式的原理和实现原理很简单,我们来看一下它的代码实现。适配器模式有两种实现:类适配器和对象适配器。其中,类适配器是使用继承关系实现的,对象适配器是使用组合关系实现的。具体代码如下。其中ITarget表示要转化成的接口定义。Adaptee是一组与ITarget接口定义不兼容的接口。Adapter将Adaptee转化为一组符合ITarget接口定义的接口。//类适配器:基于继承publicinterfaceITarget{voidf1();无效f2();voidfc();}publicclassAdaptee{publicvoidfa(){...}publicvoidfb(){...}publicvoidfc(){...}}publicclassAdapterextendsAdapteeimplementsITarget{publicvoidf1(){super.fa();}publicvoidf2(){//...重新实现f2()。..}//这里fc()不需要实现,直接继承自Adaptee,这是与对象适配器最大的区别}//对象适配器:基于组合publicinterfaceITarget{voidf1();无效f2();voidfc();}publicclassAdaptee{publicvoidfa(){...}publicvoidfb(){...}publicvoidfc(){...}}publicclassAdapterimplementsITarget{privateAdapteeadaptee;publicAdaptor(Adapteeadaptee){this.adaptee=adaptee;}publicvoidf1(){adaptee.fa();//委托给Adaptee}publicvoidf2(){//...重新实现f2()..}publicvoidfc(){adaptee.fc();}}根据这两种实现方式,在实际开发中,我们应该选择哪一种呢?判断标准主要有两个,一个是Adaptee接口,另一个是Adaptee和ITarget的契合度:如果Adaptee接口不多,两种实现都可以。如果Adaptee的接口很多,而且Adaptee和ITarget的接口定义大部分是一样的,那我们推荐使用类适配器,因为Adapter复用了父类Adaptee的接口,Adapter的代码量比对象适配器的实现。如果Adaptee的接口很多,而且Adaptee和ITarget的接口定义大多不同,那么我们推荐使用对象适配器,因为组合结构比继承更灵活。Adapter模式应用场景总结一般来说,Adapter模式可以看作是一种“补偿模式”,用来弥补设计缺陷。应用这种模式被认为是“无奈之举”。如果我们在设计初期就能够协调避免接口不兼容的问题,那么这种模式就没有应用的机会。适配器模式的应用场景是“接口不兼容”。在实际开发中,什么情况下会出现接口不兼容?封装有缺陷的接口设计假设我们所依赖的外部系统在接口设计上存在缺陷(比如包含大量的静态方法),引入后会影响我们自己代码的可测试性。为了隔离设计缺陷,我们希望对外部系统提供的接口进行重新封装,抽象出更好的接口设计。这时候就可以使用适配器模式了。例如:publicclassCD{//这个类来自外部sdk,我们无权修改它的代码//...publicstaticvoidstaticFunction1(){//...}publicvoiduglyNamingFunction2(){//...}publicvoidtooManyParamsFunction3(intparamA,intparamB,...){//...}publicvoidlowPerformanceFunction4(){//...}}//使用适配器模式重构publicclassITarget{voidfunction1();无效函数2();voidfucntion3(ParamsWrapperDefinitionparamsWrapper);无效功能4();//...}//注意:适配器类的名称不必以Adaptor结尾publicclassCDAdaptorextendsCDimplementsITarget{//...publicvoidfunction1(){super.静态函数1();}publicvoidfunction2(){超级。丑陋的命名函数2();}publicvoidfunction3(ParamsWrapperDefinitionparamsWrapper){超级。tooManyParamsFunction3(paramsWrapper.getParamA(),...);}publicvoidfunction4(){//...reimplementit...}}统一多个类的接口设计某个功能的实现依赖于多个外部系统(或类)。通过适配器模式,将它们的接口适配成一个统一的接口定义,然后我们就可以利用多态特性来复用代码逻辑。例如:假设我们的系统需要过滤用户输出的文本内容中的敏感词。为了提高过滤的召回率,我们引入了多个第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是每个系统提供的过滤接口是不同的,这就意味着我们不能复用一套逻辑去调用每个系统。这时候我们可以使用适配器模式,将所有系统的接口适配为一个统一的接口定义,这样我们就可以复用调用敏感词过滤的代码:publicclassASensitiveWordsFilter{//敏感词过滤系统提供的接口//text为原文,函数输出敏感词替换为***后的文本publicStringfilterSexyWords(Stringtext){//...}publicStringfilterPoliticalWords(Stringtext){//...}}publicclassBSensitiveWordsFilter{//B敏感词过滤系统提供的接口publicStringfilter(Stringtext){//...}}publicclassCSensitiveWordsFilter{//C敏感词过滤系统提供的接口publicStringfilter(Stringtext,Stringmask){//...}}//没有使用适配器模式之前的代码:代码的可测试性和可扩展性不好privateBSensitiveWordsFilterbFilter=newBSensitiveWordsFilter();私人CSensitiveWordsFiltercFilter=newCSensitiveWordsFilter();publicStringfilterSensitiveWords(Stringtext){StringmaskedText=aFilter.filterSexyWords(text);maskedText=aFilter.filterPoliticalWords(maskedText);maskedText=bFilter.filter(maskedText);马斯kedText,"***");返回掩码文本;}}//使用适配器模式进行转换publicinterfaceISensitiveWordsFilter{//统一接口定义Stringfilter(Stringtext);}publicStringfilter(Stringtext){StringmaskedText=aFilter.filterSexyWords(text);maskedText=aFilter.filterPoliticalWords(maskedText);返回掩码文本;}}//...省略BSensitiveWordsFilterAdaptor,CSensitiveWordsFilterAdaptor...//扩展性更好,更符合开闭原则,如果新增敏感词过滤系统,//这个类不需要完全改变了;并且基于接口而不是实现编程,代码更可测试publicclassRiskManagement{privateListfilters=newArrayList<>();publicvoidaddSensitiveWordsFilter(ISensitiveWordsFilterfilter){filters.add(filter);}publicStringfilterSensitiveWords(Stringtext){StringmaskedText=Word;for(ISensitivefilter:filters){maskedText=filter.filter(maskedText);}返回掩码文本;}}替换依赖的外部系统代码更改。如下://ExternalsystemApublicinterfaceIA{//...voidfa();}publicclassAimplementsIA{//...publicvoidfa(){//...}}//在我们的项目中使用外部系统A示例publicclassDemo{privateIAa;publicDemo(IAa){this.a=a;}//...}Demod=newDemo(newA());//用外部系统B替换外部系统ApublicclassBAdaptorimplemntsIA{privateBb;公共BAdaptor(Bb){this.b=b;}publicvoidfa(){//...b.fb();}}//借助BAdaptor,在Demo的代码中,调用IA接口的地方无需改动,//只需要将BAdaptor注入到Demo中,如下所示。Demod=newDemo(newBAdaptor(newB()));兼容旧版本接口在进行版本升级时,我们不会直接删除一些要丢弃的接口,而是暂时保留并标记为deprecated,并将内部实现逻辑委托给新的接口实现。这样做的好处是它可以让使用它的项目有一个过渡期,而不是强制更改代码。这也可以大致看成是适配器模式的一个应用场景。例如:JDK1.0中包含一个遍历集合容器的类Enumeration。JDK2.0重构了这个类,改名为Iterator类,优化了代码实现。但是考虑到如果直接从JDK2.0删除Enumeration,如果使用JDK1.0的项目切换到JDK2.0,代码会编译失败。为了避免这种情况的发生,我们必须将项目中所有使用Enumeration的地方修改为使用Iterator。单个项目将Enumeration替换为Iterator是勉强可以接受的。但是,用Java开发的项目太多了。一个JDK升级后,所有项目都编译报错,不修改代码,这显然是不合理的。这就是我们常说的不兼容升级。为了兼容使用低版本JDK的旧代码,我们可以暂时保留Enumeration类,直接调用Itertor替换其实现。代码示例如下所示:publicbooleanhasMoreElments(){返回i.hashNext();}publicObjectnextElement(){returni.next():}}}}适配不同格式的数据适配器模式不仅可以用于接口适配,还可以用于不同格式数据之间的适配。例如,将从不同征信系统中拉取的不同格式的征信数据统一为相同的格式,便于存储和使用。比如Java中的Arrays.asList()也可以看作是一个数据适配器,将数组类型的数据转换为集合容器类型。Liststooges=Arrays.asList("Larry","Moe","Curly")分析适配器模式在Java日志中的应用Java中有很多日志框架,我们经常使用它们来打印日志信息项目发展。其中比较常用的有JDK提供的log4j、logback、JUL(java.util.logging)和Apache提供的JCL(JakartaCommonsLogging)。大多数日志框架都提供了类似的功能,比如按照不同的级别(debug、info、warn、erro...)打印日志,但它们并没有实现统一的接口。这可能主要是由于历史原因。与JDBC不同的是,它从一开始就制定了数据库操作的接口规范。如果我们只是开发一个项目供自己使用,那么任何日志框架都可以。但是如果我们开发一个组件、框架、类库等集成到其他系统中,那么日志框架的选择就没有那么随意了。比如项目中使用的一个组件使用log4j打印日志,而我们的项目本身使用logback。在项目中引入组件后,我们的项目就相当于拥有了两套日志打印框架。每个日志记录框架都有自己独特的配置。因此,我们需要为每个日志框架编写不同的配置文件(例如日志存储的文件地址,打印日志的格式)。如果引入多个组件,每个组件使用不同的日志框架,那么日志本身的管理就变得非常复杂。所以,为了解决这个问题,我们需要同样的日志打印框架。java中的Slf4j日志框架相当于JDBC规范,提供了一套统一的打印日志的接口规范。但是,它只定义了接口,并没有提供具体的实现。它需要与其他日志框架(log4j、logback...)一起使用。不仅如此,Slf4j的出现晚于JUL、JCL、log4j等日志框架,所以这些日志框架不应牺牲版本兼容性,对接口进行改造以符合Slf4j接口规范。Slf4j也提前考虑到了这个问题,所以它不仅提供了统一的接口定义,还提供了针对不同日志框架的适配器,将不同日志框架的接口重新封装,适配成统一的Slf4j接口定义。具体代码如下://slf4j统一接口包org.slf4j;publicinterfaceLogger{publicbooleanisTraceEnabled();公共无效跟踪(字符串消息);publicvoidtrace(字符串格式,对象参数);publicvoidtrace(字符串格式,对象arg1,对象arg2);publicvoidtrace(字符串格式,对象[]argArray);publicvoidtrace(Stringmsg,Throwablet);publicbooleanisDebugEnabled();publicvoiddebug(Stringmsg);publicvoiddebug(字符串格式,对象参数);publicvoiddebug(Stringformat,Objectarg1,Objectarg2)publicvoiddebug(Stringformat,Object[]argArray)publicvoiddebug(Stringmsg,Throwablet);//...省略了一堆info、warn、error等接口}//log4j日志框架适配器//Log4jLoggerAdapter实现了LocationAwareLogger接口,//其中LocationAwareLogger继承自Logger接口,//相当于Log4jLoggerAdapter实现记录器接口。packageorg.slf4j.impl;publicfinalclassLog4jLoggerAdapterextendsMarkerIgnoringBaseimplementsLocationAwareLogger,Serializable{finaltransientorg.apache.log4j.Logger记录器;//log4jpublicbooleanisDebugEnabled(){returnlogger.isDebugEnabled();}publicvoiddebug(Stringmsg){logger.log(FQCN,Level.DEBUG,msg,null);}publicvoiddebug(Stringformat,Objectarg){if(logger.isDebugEnabled()){FormattingTupleft=MessageFormatter.format(format,arg);logger.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());}}publicvoiddebug(Stringformat,Objectarg1,Objectarg2){if(logger.isDebugEnabled()){FormattingTupleft=MessageFormatter.format(format,arg1,arg2);logger.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());}}publicvoiddebug(Stringformat,Object[]argArray){if(logger.isDebugEnabled()){FormattingTupleft=MessageFormatter.arrayFormat(format,argArray);记录器.log(FQCN,Level.DEBUG,ft.getMessage(),ft.getThrowable());}}publicvoiddebug(Stringmsg,Throwablet){logger.log(FQCN,Level.DEBUG,msg,t);}//...省略了一堆接口的实现...}所以在开发业务系统或者开发框架和组件的时候,我们统一使用Slf4j提供的接口来编写打印日志的代码,日志框架要具体使用(log4j、logback...),可以动态指定,只需要在项目中导入相应的SDK即可。Proxy、Bridge、Decorator、Adapter4种设计模式的区别Proxy、Bridge、Decorator、Adapter,这4种模式是比较常用的结构化设计模式。它们的代码结构非常相似。一般来说,它们都可以称为Wrapper模式,即通过Wrapper类对原有类进行二次封装。虽然代码结构相似,但这四种设计模式的用意是完全不同的,也就是说要解决的问题和应用场景不同,这也是它们的主要区别。代理模式:代理模式在不改变原类接口的情况下,为原类定义一个代理类。主要目的是控制访问,而不是增强功能。这是它与装饰者模式最大的区别。桥接模式:桥接模式的目的是将接口部分和实现部分分开,使它们更容易更改,相对独立。装饰器模式:装饰器模式在不改变原有类接口的情况下,增强了原有类的功能,支持多种装饰器模式的嵌套使用。适配器模式:适配器模式是一种事后补救策略。适配器提供与原始类不同的接口,而代理模式和装饰器模式提供与原始类相同的接口。总结适配器模式用于适配。它将不兼容的接口转换为兼容的接口,使得因接口不兼容而无法协同工作的类可以协同工作。适配器模式有两种实现:类适配器和对象适配器。其中,类适配器是使用继承关系实现的,对象适配器是使用组合关系实现的。一般来说,适配器模式可以看作是一种弥补设计缺陷的“补偿模式”。应用这种模式是一种“无奈之举”。如果我们在设计初期就能够协调避免接口不兼容的问题,那么这种模式就没有应用的机会。那么在实际开发中,在什么情况下会出现接口不兼容呢?封装有缺陷的接口设计。统一多个类的接口设计。替换依赖的外部系统。兼容老版本界面。适应不同格式的数据。