写点什么

07. 适配器模式设计思想

作者:杨充
  • 2024-10-31
    湖北
  • 本文字数:14419 字

    阅读完需:约 47 分钟

07.适配器模式设计思想

目录介绍

  • 01.适配器模式基础

  • 1.1 适配器模式由来

  • 1.2 适配器模式定义

  • 1.3 适配器模式场景

  • 1.4 适配器模式思考

  • 02.适配器模式实现

  • 2.1 罗列一个场景

  • 2.2 用例子理解适配器

  • 2.3 适配器基本实现

  • 2.4 如何选择适配器

  • 03.适配器模式分析

  • 3.1 类适配器案例

  • 3.2 对象适配器案例

  • 3.3 适配器模式结构图

  • 3.4 适配器模式时序图

  • 04.适配器应用解析

  • 4.1 读卡器适配案例【类】

  • 4.2 类适配器优缺点

  • 4.3 播放器适配器案例【对象】

  • 4.4 对象适配器优缺点

  • 05.实际场景演变

  • 5.1 封装有缺陷的接口设计

  • 5.2 设计统一多个类的接口

  • 5.3 替换依赖的外部系统

  • 5.4 兼容老版本接口

  • 5.5 适配不同格式的数据

  • 5.6 Java 适配器模式使用

  • 5.7 数据库适配器

  • 5.8 日志框架适配器

  • 5.9 第三方库适配器

  • 06.适配器总结一下

  • 6.1 适配器使用环境

  • 6.2 适配器模式拓展

  • 6.3 跟其他模式对比

  • 07.动态代理总结

  • 7.1 总结一下学习

  • 7.2 更多内容推荐

推荐一个好玩网站

一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网


https://yccoding.com/

01.适配器模式基础

1.0 本博客 AI 摘要

适配器模式是一种结构型设计模式,用于将不兼容的接口转换为可兼容的接口,使原本不能一起工作的类可以协同工作。本文详细介绍了适配器模式的基础、实现方式(类适配器和对象适配器)、应用场景(如封装有缺陷的接口、统一多个类的接口、替换依赖的外部系统等)以及优缺点。通过具体案例(如读卡器适配、播放器适配)和实际开发中的应用(如数据库适配器、日志框架适配器),帮助读者深入理解和应用适配器模式。

1.1 适配器模式由来

适配器模式的英文翻译是 Adapter Design Pattern。


顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。


对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。


主要解决问题:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。


简单来说,用于事后补救措施,项目代码后期,想让不想关的类,变成可以一起工作

1.2 适配器模式定义

适配器模式(Adapter Pattern)的定义 :


  1. 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

  2. 适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

1.3 适配器模式场景

在计算机编程中,当我们有两个已有的功能,但由于某种原因它们不能直接协同工作时,我们使用适配器模式。


举个例子 1:类似 USB 转接头,既可以链接 Android 手机,也可以链接 IOS 手机。


举个例子 2:欧洲制造的电器,但你住在中国。你不能直接将这部电器插入墙上,因为插头和插座不兼容。此时,你需要一个适配器,将欧洲插头转换为中国插座。

1.4 适配器模式思考

适配器模式应该在真正需要时使用,而不是为了强行适配而使用。


在设计阶段,应该尽量考虑接口的一致性和兼容性,以减少对适配器模式的依赖。

02.适配器模式原理与实现

2.1 罗列一个场景

适配器模式在软件开发中有多种应用场景,以下是一些常见的应用场景:


  1. 数据库访问:适配器模式可以用于将不同数据库供应商的 API 转换为统一的数据库访问接口,以便在不同的数据库之间切换和使用。

  2. 旧系统升级:当需要将旧系统升级为新系统时,适配器模式可以用于保留旧系统的功能,并将其接口转换为新系统所需的接口。

  3. 第三方库使用:当使用第三方库或组件时,其提供的接口可能与当前系统的接口不兼容。适配器模式可以用于将第三方库的接口转换为系统所需的接口。

2.2 用例子理解适配器

适配器有两种方式:


  1. 类的适配器。通过继承来实现

  2. 对象的适配器。通过组合来实现


具体的代码实现如下所示。


其中,ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口。

2.3 适配器模式基本实现

类的适配器。通过继承来实现


●目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。


●源(Adapee)角色:现在需要适配的接口。


●适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。


// 类适配器: 基于继承public interface ITarget {    void f1();    void f2();    void f3();}
public class Adaptee { public void fa() { //... }
public void fb() { //... }
public void fc() { //... }}
public class Adaptor extends Adaptee implements ITarget { public void f1() { super.fa(); System.out.println("调用适配器中方法"); }
public void f2() { //...重新实现f2()... super.fb(); System.out.println("重新实现f2"); }
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点 @Override public void f3() {
}}
复制代码


对象的适配器。通过组合来实现


// 对象适配器:基于组合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 final Adaptee adaptee;
public Adaptor(Adaptee adaptee) { this.adaptee = adaptee; }
public void f1() { adaptee.fa(); //委托给Adaptee }
public void f2() { //...重新实现f2()... }
public void fc() { adaptee.fc(); }}
复制代码

2.4 如何选择适配器

针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?


判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 Adaptee 和 ITarget 的契合程度。


  1. 如果 Adaptee 接口并不多,那两种实现方式都可以。

  2. 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。

  3. 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

03.适配器模式分析

3.1 类适配器案例

类适配器是一种结构型设计模式,它允许将一个类的接口转换为另一个客户端所期望的接口。通过继承原始类和实现目标接口,类适配器使得原始类的接口与目标接口兼容。


特点: 1.使用继承:类适配器通过继承源类来实现适配功能。2.单一适配:由于 Java 中不支持多重继承,类适配器只能适配一个源类。

3.2 对象适配器案例

对象适配器是适配器模式的一种变体,它通过组合(而不是继承)原始类和目标接口来实现适配器功能。在对象适配器中,适配器类持有一个原始类的实例,并实现目标接口,通过调用原始类的方法来实现目标接口的方法。


特点:1.适配器通过组合原始类的实例来实现适配器功能,而不是通过继承原始类。2.对象适配器可以重用现有的功能,无需修改原始类的代码。

3.3 适配器模式结构图

适配器模式包含如下角色:


  1. Target:目标抽象类。定义客户端所期望的接口。适配器将原始类的接口转换为目标接口,使得客户端可以通过目标接口来访问原始类的功能。

  2. Adapter:适配器类。适配器类实现了目标接口,并持有原始类的实例。适配器类中的方法通过调用原始类的方法来实现目标接口的方法。适配器充当了目标接口和原始类之间的桥梁。

  3. Adaptee:适配者类【原始类】。需要被适配的类,它定义了不兼容目标接口的方法。适配器模式的目标是使得客户端可以通过目标接口来使用原始类的功能。

  4. Client:客户类。客户端可以通过目标接口来使用原始类的功能,而不需要直接与原始类交互。

3.4 适配器模式时序图

适配器模式时序图如下所示:

04.适配器应用解析

4.1 读卡器适配案例【类】

读卡器需求分析:


现有一台电脑只能读取 SD 卡,而要读取 TF 卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将 TF 卡中的内容读取出来。


首先我们先开发电脑读取 SD 卡的业务:


Computer 只能使用 SD 卡,所以方法名为 readSD(),需要 SDCard 类型的对象。具体做法是:主要是 SDCard(包含两个方法:读数据,写数据),子实现类为 SDCardImpl。


public interface SDCard {    //从SD卡中读取数据    String readSD();
//往SD卡中写数据 void writeSD(String msg);}
//具体的SD卡public class SDCardImpl implements SDCard { @Override public String readSD() { String msg = "SDCard read msg : SD"; System.out.println(msg); return msg; }
@Override public void writeSD(String msg) { System.out.println("SDCard write msg : " + msg); }}
//计算机类public class Computer {
//从SD卡中读取数据 public String readSD(SDCard sdCard) { if (sdCard == null) { throw new NullPointerException("sd card is null"); } return sdCard.readSD(); }}
private void test1() { //计算机读SD卡 Computer computer = new Computer(); SDCardImpl sdCard = new SDCardImpl(); computer.readSD(sdCard);}
//输出结果是://SDCard read msg : SD
复制代码


然后,思想一下如何给 TF 做适配。


  1. 第一步:定义了一个接口 TFCard 接口,有两个方法(读取数据,写数据)。子实现类 TFCardImpl 重写了接口的两个方法

  2. 第二步:当我们想用 Computer 去读取 TF 卡中的内容,不能直接读取,需要定义适配器类。要让这个适配器类实现目标接口,就要重写 SDCard 中的两个方法,同时我们要让它去继承 TFCardImpl。

  3. 第三步:最后,实现之后这两个方法看似是从 SD 卡中读数据写数据,但是实际上用的是 TF 卡中的功能


//适配者类的接口public interface TFCard {    //从TF卡中读取数据    String readTF();    //往TF卡中写数据    void writeTF(String msg);}
//适配者类public class TFCardImpl implements TFCard {
@Override public String readTF() { String msg = "TFCard read msg : TF"; System.out.println(msg); return msg; }
@Override public void writeTF(String msg) { System.out.println("TFCard write msg : " + msg); }}
//适配器类public class SDAdapterTF extends TFCardImpl implements SDCard {
@Override public String readSD() { System.out.println("adapter read tf card"); return readTF(); }
@Override public void writeSD(String msg) { System.out.println("adapter wrete tf card"); writeTF(msg); }}
private void test2() { //计算器适配,读卡器,去读TF卡中的内容 Computer computer = new Computer(); //创建适配器对象,完全没有影响之前计算机读SD卡的逻辑 SDAdapterTF sdAdapterTF = new SDAdapterTF(); computer.readSD(sdAdapterTF);}
复制代码


通过这个案例可以知道


适配器模式是一种强大的设计模式,能够有效解决接口不兼容的问题,使得不同接口的类能够协同工作。通过合理使用适配器模式,可以提高系统的灵活性和复用性,但也需要注意其可能带来的复杂性和性能影响。

4.2 类适配器优缺点

优点


  1. 简单实现:由于类适配器通过继承实现,它可以直接访问被适配类(Adaptee)的所有方法。这使得实现适配器时相对简单,不需要额外的委托逻辑。

  2. 提高代码的可复用性:适配器模式通过继承实现,使得子类能够继承父类的所有功能,从而提高了代码的复用性。

  3. 可以重定义被适配类的一些行为:通过继承,可以在适配器类中重定义被适配类的一些方法,实现更加灵活的适配。


缺点


  1. 受限于单继承:类适配器模式在 Java 等单继承语言中有一个显著的缺点,即一个类只能继承一个父类。这意味着如果适配器类已经有一个父类,就不能再使用类适配器模式来继承另一个类。

  2. 高耦合:适配器类和被适配类之间的耦合度较高,因为适配器类直接继承了被适配类的实现。如果被适配类发生变化,适配器类可能也需要进行相应的修改。

  3. 不符合“组合优于继承”原则:面向对象设计中,组合优于继承的原则提倡使用组合来代替继承,以降低类之间的耦合度。类适配器模式违背了这一原则,因为它是通过继承来实现适配的。

4.3 播放器适配器案例【对象】

播放器案例需求分析


想象一下,我们有一个 MediaPlayer 接口,它可以播放 mp3 格式的文件。


private void test1() {    MediaPlayer mp3Player = new Mp3Player();    mp3Player.play("mp3","file.mp3");}
// MediaPlayer.javapublic interface MediaPlayer { void play(String audioType, String fileName);}
// Mp3Player.javapublic class Mp3Player implements MediaPlayer { @Override public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("mp3")) { System.out.println("Playing mp3 file. Name: " + fileName); } else { System.out.println("Invalid media. " + audioType + " format not supported"); } }}
复制代码


现在,我们想扩展这个功能,使其也可以播放其他格式的文件,比如 vlc 和 mp4。但是,我们不希望修改原始的 MediaPlayer 接口。


为了支持更多的格式,我们将创建一个新的接口 AdvancedMediaPlayer:


private void test2() {    AdvancedMediaPlayer vlcPlayer = new VlcPlayer();    vlcPlayer.playVlc("vlc");    AdvancedMediaPlayer mp4Player = new Mp4Player();    mp4Player.playMp4("mp4");}
// AdvancedMediaPlayer.javapublic interface AdvancedMediaPlayer { void playVlc(String fileName); void playMp4(String fileName);}
// VlcPlayer.javapublic class VlcPlayer implements AdvancedMediaPlayer { @Override public void playVlc(String fileName) { System.out.println("Playing vlc file. Name: " + fileName); }
@Override public void playMp4(String fileName) { // Do nothing }}
// Mp4Player.javapublic class Mp4Player implements AdvancedMediaPlayer { @Override public void playVlc(String fileName) { // Do nothing }
@Override public void playMp4(String fileName) { System.out.println("Playing mp4 file. Name: " + fileName); }}
复制代码


现在,我们已经有了基础的播放器,但我们还需要一个适配器,以便 MediaPlayer 可以使用 AdvancedMediaPlayer。


实现 MediaPlayer 适配器。为了使 MediaPlayer 能够使用 AdvancedMediaPlayer,我们需要创建一个适配器。这个适配器将决定使用哪个 AdvancedMediaPlayer 的实现。


public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) { if (audioType.equalsIgnoreCase("vlc")) { advancedMusicPlayer = new VlcPlayer(); } else if (audioType.equalsIgnoreCase("mp4")) { advancedMusicPlayer = new Mp4Player(); } }
@Override public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("vlc")) { advancedMusicPlayer.playVlc(fileName); } else if (audioType.equalsIgnoreCase("mp4")) { advancedMusicPlayer.playMp4(fileName); } }}
复制代码


现在,让我们扩展我们的 Mp3Player 类,使其可以使用 MediaAdapter 来播放其他格式:


public class AudioPlayer implements MediaPlayer {    MediaAdapter mediaAdapter;
@Override public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("mp3")) { System.out.println("Playing mp3 file. Name: " + fileName); } else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) { mediaAdapter = new MediaAdapter(audioType); mediaAdapter.play(audioType, fileName); } else { System.out.println("Invalid media. " + audioType + " format not supported"); } }}
复制代码


让我们通过一些测试来验证我们的适配器是否正常工作:


private void test3() {    AudioPlayer audioPlayer = new AudioPlayer();    audioPlayer.play("mp3", "mySong.mp3");    audioPlayer.play("mp4", "video.mp4");    audioPlayer.play("vlc", "movie.vlc");    audioPlayer.play("avi", "doubi.avi");  // Not supported}
复制代码


适配器模式是一个非常有用的模式,它允许我们整合不兼容的接口,而不需要修改原始代码。在本例中,我们成功地扩展了 MediaPlayer 的功能,使其可以播放其他格式的文件,而不需要改变其原始定义。


像上面的 MediaPlayer 示例一样,你可能会在许多真实的应用程序中遇到类似的场景,其中一些旧的接口需要与新的接口一起工作,但又不希望进行大规模的重写。

4.4 对象适配器优缺点

对象适配器模式具有以下优点:


  1. 适配器可以重用现有的功能,无需修改原始类的代码。适配器通过将原始类的接口转换为目标接口,使得客户端可以无缝地使用原始类的功能。

  2. 对象适配器使用组合来实现适配器功能,因此可以适配原始类的子类。这使得对象适配器具有更大的灵活性和可扩展性。

  3. 对象适配器可以在不修改客户端代码的情况下实现接口转换。客户端只需要通过调用适配器类的方法来使用原始类的功能,而不需要直接与原始类交互。

  4. 对象适配器可以适配多个不同的原始类,只需创建相应的适配器类即可。这使得对象适配器具有更高的复用性和可维护性。


然而,对象适配器模式也有一些缺点和限制:


  1. 对象适配器引入了额外的对象引用,可能会增加内存消耗。每个适配器实例都需要持有一个原始类的实例。

  2. 对象适配器在适配器类中引入了原始类的功能,可能会导致接口的冗余和复杂性增加。

  3. 对象适配器需要通过组合来实现适配器功能,这可能需要更多的代码和配置。

05.应用场景分析

大概有哪些场景


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

  2. 统一多个类的接口设计


为何用这个


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

5.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(); //...}// 注意:适配器类的命名不一定非得末尾带Adaptorpublic 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... }}
复制代码

5.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<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; }}
复制代码

5.3 替换依赖的外部系统

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


// 外部系统Apublic 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替换成外部系统Bpublic 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()));
复制代码

5.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.5 适配不同格式的数据

前面我们讲到,适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。


比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。


再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。


List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
复制代码

5.6 Java 适配器模式使用

JDK1.1 之前提供的容器有 Arrays,Vector,Stack,Hashtable,Properties,BitSet,其中定义了一种访问群集内各元素的标准方式,称为 Enumeration(列举器)接口。


Vector v=new Vector();for (Enumeration enum =v.elements(); enum.hasMoreElements();) {  Object o = enum.nextElement();  processObject(o);}
复制代码


JDK1.2 版本中引入了 Iterator 接口,新版本的集合对(HashSet,HashMap,WeakHashMap,ArrayList,TreeSet,TreeMap, LinkedList)是通过 Iterator 接口访问集合元素。


List list=new ArrayList();for(Iterator it=list.iterator();it.hasNext();){   System.out.println(it.next());}
复制代码


这样,如果将老版本的程序运行在新的 Java 编译器上就会出错。因为 List 接口中已经没有 elements(),而只有 iterator() 了。


那么如何将老版本的程序运行在新的 Java 编译器上呢? 如果不加修改,是肯定不行的,但是修改要遵循“开-闭”原则。我们可以用 Java 设计模式中的适配器模式解决这个问题。


public class NewEnumeration implements Enumeration {    Iterator it;      public NewEnumeration(Iterator it) {        this.it = it;    }      public boolean hasMoreElements() {        return it.hasNext();    }      public Object nextElement() {        return it.next();    }      public static void main(String[] args) {        List list = new ArrayList();        list.add("a");        list.add("b");        list.add("C");        for (Enumeration e = new NewEnumeration(list.iterator()); e.hasMoreElements(); ) {            System.out.println(e.nextElement());        }    }}
复制代码


NewEnumeration 是一个适配器类,通过它实现了从 Iterator 接口到 Enumeration 接口的适配,这样我们就可以使用老版本的代码来使用新的集合对象了。

5.7 数据库适配器

在许多应用程序中,需要与多种数据库进行交互。如果每种数据库都有不同的接口,那么使用适配器模式可以简化代码,并提高代码的可维护性和可扩展性。例如:


public interface Database {    void connect();    void query(String sql);}
public class MySqlDatabase implements Database { @Override public void connect() { System.out.println("Connecting to MySQL Database"); }
@Override public void query(String sql) { System.out.println("Querying data from MySQL Database: " + sql); }}
public class OracleDatabaseAdapter implements Database { private OracleDatabase oracleDatabase;
public OracleDatabaseAdapter(OracleDatabase oracleDatabase) { this.oracleDatabase = oracleDatabase; }
@Override public void connect() { oracleDatabase.open(); }
@Override public void query(String sql) { oracleDatabase.execute(sql); }}
class OracleDatabase { void open() { System.out.println("Opening Oracle Database"); }
void execute(String sql) { System.out.println("Executing SQL on Oracle Database: " + sql); }}
复制代码

5.8 日志框架适配器

许多 Java 应用程序使用日志框架来记录应用程序的运行情况。有许多不同的日志框架,比如 Log4j、SLF4J 等,它们有着不同的 API。通过使用适配器模式,我们可以定义一个统一的日志接口,然后为每个日志框架实现一个适配器,从而让应用程序可以在不同的日志框架之间无缝切换。


public interface Logger {    void log(String message);}
public class Log4jAdapter implements Logger { private org.apache.log4j.Logger logger;
public Log4jAdapter(org.apache.log4j.Logger logger) { this.logger = logger; }
@Override public void log(String message) { logger.info(message); }}
public class SLF4JAdapter implements Logger { private org.slf4j.Logger logger;
public SLF4JAdapter(org.slf4j.Logger logger) { this.logger = logger; }
@Override public void log(String message) { logger.info(message); }}
复制代码


这样,我们就可以在应用程序中自由切换使用哪个日志框架,而不需要修改大量的代码。

5.9 第三方库适配器

在实际开发中,我们经常会用到第三方库。但是,不同的库可能提供了不同的 API,直接使用会导致代码的耦合度增加。通过适配器模式,我们可以为每个库提供一个适配器,使它们都符合同一个接口,这样在主程序中就可以无缝切换,降低了代码的耦合度。

06.适配器总结一下

6.1 适配器使用环境

在以下情况下可以使用适配器模式:


  1. 系统需要使用现有的类,而这些类的接口不符合系统的需要。

  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

6.2 适配器模式拓展

认适配器模式(Default Adapter Pattern)或缺省适配器模式


当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。因此也称为单接口适配器模式。


一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷


应用这种模式算是“无奈之举”,如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

6.3 跟其他模式对比

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。


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


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

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

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

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

07.动态代理总结

7.1 总结一下笔记

01.适配器模式基础


什么叫做适配器模式?用来做适配,将不兼容的接口转化成兼容的接口,例子就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。


简单来说,用于事后补救措施,项目代码后期,想让不想关的类,变成可以一起工作。可以作为类结构型模式,也可以作为对象结构型模式。


适配器模式思考?不要强行适配,设计前期就要考虑到兼容性,以减少对适配器依赖!


02.适配器模式实现


适配器模式两种实现方式:


  1. 类的适配器。通过继承来实现

  2. 对象的适配器。通过组合来实现


03.适配器模式分析


类适配器是一种结构型设计模式,它允许将一个类的接口转换为另一个客户端所期望的接口。通过继承原始类和实现目标接口,类适配器使得原始类的接口与目标接口兼容。


对象适配器是适配器模式的一种变体,它通过组合(而不是继承)原始类和目标接口来实现适配器功能。在对象适配器中,适配器类持有一个原始类的实例,并实现目标接口,通过调用原始类的方法来实现目标接口的方法。


04.适配器应用解析


类适配器:现有一台电脑只能读取 SD 卡,而要读取 TF 卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将 TF 卡中的内容读取出来。


对象适配器:有一个 MediaPlayer 接口,它可以播放 mp3 格式的文件。现在,我们想扩展这个功能,使其也可以播放其他格式的文件,比如 vlc 和 mp4。


05.应用场景分析


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


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

  2. 设计统一多个类的接口。

  3. 替换依赖的外部系统。

  4. 兼容老版本接口。

  5. 适配不同格式的数据。

7.2 更多内容推荐


23 种设计模式


7.3 更多内容


发布于: 刚刚阅读数: 5
用户头像

杨充

关注

还未添加个人签名 2018-07-30 加入

还未添加个人简介

评论

发布
暂无评论
07.适配器模式设计思想_杨充_InfoQ写作社区