10.适配器设计模式

315 阅读7分钟

一. 什么是适配器模式?

1.1 概念

适配器模式是将一个类的接口转换成客户希望的另外一个接口. Adapter模式可以使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

适配器模式的主要作用是: 把一个类的接口变换成客户端所期待的另一种接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作。

1.2 适配器模式解决的是什么问题 ?

简单地说, 就是需要的东西就在面前,但是却不能使用, 而短时间又无法改造它, 于是我们就要想办法适配他.

二. 案例

举个例子, 我们以前都使用过mp3, 最开始mp3只能播放mp3格式的音频文件. 后来又有了mp4格式的音频文件, vlc格式的音频文件. 相应的又出了超级媒体播放器, 可以用来播放vlc和mp4格式的音乐. 这么多播放器, 拿着太不方便 , 要是能让MP3播放器能够直接播放mp4和vlc格式的音频文件, 那就好了, 使用起来更方便 .

需求: 让MP3播放器拥有超媒体播放器播放mp4和vlc格式文件的功能

来看看UML图:

1. MP3格式的播放器

有一个MediaPlayer接口, AudioPlayer音乐播放器实现了MediaPlayer接口

源码如下:


/**
 * 媒体播放器接口
 */
public interface MediaPlayer {
    /**
     * 媒体播放器可以播放音乐
     */
    void play(String type);
}


/**
 * mp3格式的音乐播放器
 */
public class AudioPlayer implements MediaPlayer {
    /**
     * 可以播放MP3格式的音乐
     *
     * @param type
     */
    @Override
    public void play(String type) {
        if ("mp3".equals(type)) {
            System.out.println("播放mp3格式的音乐");
        } else  {
            System.out.println(type+"格式的音乐, 播放器暂不支持");
        }
    }
}

这个播放器只能播放mp3格式的音乐

2. 超媒体播放器, 可以播放vlc和MP4音乐

这里有一个超媒体播放器接口和vlcAndMp4格式的超媒体播放器.


/**
 * 超级媒体播放器接口
 */
public interface AdviceMediaPlayer {
    /**
     * 播放音乐
     *
     * 这里使用broadcast主要是想区别于MediaPlay的play.
     */
    void broadcast(String type);
}


public class VlcOrMp4AdviceMediaPlayer implements AdviceMediaPlayer{
    @Override
    public void broadcast(String type) {
        if ("mp4".equals(type)) {
            System.out.println("播放MP4音乐");
        } else if ("vlc".equals(type)) {
            System.out.println("播放vlc音乐");
        } else {
            System.out.println("不支持的格式....");
        }
    }
}

这个播放器只能播放mp4和vlc格式的音乐.

3. 让MP3格式的播放器可以播放mp4和vlc格式的音乐.

来看看使用适配器模式以后的UML图:

我们现在定义一个适配器, 这个适配器将超媒体格式的音频转换为mediaPlayer格式, 这样在audioPlayer中就可以使用了

public class MediaAdapter implements MediaPlayer {

    private AdviceMediaPlayer adviceMediaPlayer;

    public MediaAdapter(AdviceMediaPlayer adviceMediaPlayer) {
        this.adviceMediaPlayer = adviceMediaPlayer;
    }

    @Override
    public void play(String type) {
        adviceMediaPlayer.broadcast(type);
    }
}

在媒体适配器中引用了AdviceMediaPlayer. 并将超媒体功能进行转换, 转换成普通媒体类型可接受的输出.

接下来需要重写AudioPlayer, 让其支持超媒体播放功能

package com.lxl.www.designPatterns.adapterPattern.mediaPlayNew;

import com.lxl.www.designPatterns.adapterPattern.mediaPlayOld.MediaPlayer;
import com.lxl.www.designPatterns.adapterPattern.mediaPlayOld.VlcOrMp4AdviceMediaPlayer;


public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String type) {
        if ("mp3".equals(type)) {
            System.out.println("播放MP3格式的音乐");
        } else if ("vlc".equals(type) || "mp4".equals(type)) {
            mediaAdapter = new MediaAdapter(new VlcOrMp4AdviceMediaPlayer());
            mediaAdapter.play(type);
        }
    }
}

下面来看看效果

public class MediaClient {
    public static void main(String[] args) {
        MediaPlayer mediaPlayer = new AudioPlayer();
        mediaPlayer.play("vlc");
    }
}

运行结果:

播放vlc音乐

原来mp3格式的播放器是不能播放vlc音乐的, 现在可以了.

4. 总结:

根据上面的结构我们来总结一下:

1. 适配对象: AdviceMediaPlayer 超级媒体播放器使我们需要适配的对象. 将其功能能够在普通播放器上播放

2. 目标对象: 这里的目标对象是 MediaPlayer, 他要在能够播放普通mp3文件的基础上, 还能拥有AdviceMediaPlayer超级媒体播放的功能

3. 适配器: AdviceMediaPlayer和MediaPlayer本身是相互独立的两个接口, 二者之间互不兼容. 但我们希望mediaPlay能够拥有AdviceMediaPlayer的功能, 适配器的作用是将AdviceMediaPlayer适配成MediaPlayer

4. client客户端: 在这里就是main方法. main方法可以向原来一样调用 MediaPlayer, 但是可以传入新的播放类型.

三. 适配器的类型和原理

上面大致演示了适配器模式的使用, 下面来看看适配器模式的UML图:

3.1 适配器的原理:

  1. adapteree被适配的对象: 已经存在的接口,但与客户端要求的特定领域接口不一致, 需要被适配
  2. Target目标对象: 定义特定领域的相关接口
  3. Adapter适配器: 把被适配对象适配成为需要的Target目标对象
  4. Client客户端: 调用自己需要的领域接口Target

3.2 适配器的类型

适配器有两种类型:

  1. 类适配器
  2. 对象适配器. 这两种类型是从适配的形式上区别的.

3.3 模板代码

Adaptee.java【需要被适配的接口类,也就是客户端调用最终会委派到这个类来】:


/**
 * 被适配对象
 */
public class Adapteree {
    public void speicialMethod() {
        System.out.println("被适配对象");
    }
}

Target.java【客户端最终要调用的接口】:


/**
 * 目标对象. 我们希望在目标对象上适配被适配对象
 */
public interface Target {
    void requestMethod();
}

Adapter.java【适配器模式最核心的类, 需要掌握它的写法】


/**
 * 适配器
 */
public class Adapter implements Target{
    /**
     * 被适配对象
     */
    Adapteree adapteree;

    public Adapter(Adapteree adapteree) {
        this.adapteree = adapteree;
    }


    @Override
    public void requestMethod() {
        adapteree.speicialMethod();
    }
}

这是一个对象适配器. 将其转换为类适配, 写法如下


/**
 * 适配器
 */
public class Adapter extends Adapteree implements Target {
    
    @Override
    public void requestMethod() {
        adapteree.speicialMethod();
    }
}

Client.java:

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        /**
         * 实例化的时候实例化的是适配器对象
         * 调用方法是调用目标方法
         */
        Target target = new Adapter(new Adapteree());
        target.requestMethod();
    }
}

四. 适配器的优缺点:

优点:

  1. 更好的复用性 系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
  2. 透明、简单 客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单 & 更直接
  3. 更好的扩展性 在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
  4. 解耦性 将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码
  5. 符合开放-关闭原则 同一个适配器可以把适配者类和它的子类都适配到目标接口;可以为不同的目标接口实现不同的适配器,而不需要修改待适配类

缺点:

  1. 过多的使用适配器,会让系统非常零乱,不容易整体进行把握。【尤其对于看代码的人来说,明明是一个操作数据库日志的方法,而最终会被适配成其实方式,比如文件操作,这样就很容易让人迷糊,适配器最好的使用场景就是维护阶段,在开发阶段尽量少用】

五. 适配器的本质和使用场景:

5.1 本质

转换匹配、复用功能  

5.2 使用场景:

  1、如果你想要使用一个已经存在的类,但是它的接口不符合你的要求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口。

  2、如果我想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作,这种情况可以使用适配器模式,到时候需要什么就适配什么。

注意: 通常, 在双方都不太容易修改的时候再使用适配器模式适配, 而不是一有不同就是用它。因为使用适配器模式会增加系统的复杂性。