《HF 设计模式》 C6 适配器模式&外观模式

文章目录

1 适配器模式

1.1 认识适配器

如果你想给你的手机充电,但是附近只有一个三孔插座,为此你可能需要一个插线板
《HF 设计模式》 C6 适配器模式&外观模式
这个插线板就是适配器,而OO适配器和真实世界的适配器扮演着同样的角色:将一个接口转换成另一个接口,以符合客户的期望

我们来看看JDK中常用的适配器模式:
在swing技术中,我们通常要为各种组件增加侦听器,如焦点的获取/失去:

		/*编号文本框获取焦点,编号文本框被全部选中*/
        this.jtxtId.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                jtxtId.selectAll();
            }
        });

        /*名称文本框获取焦点,名称文本框被全部选中*/
        this.jtxtName.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                jtxtName.selectAll();
            }
        });

打开FocusAdapte类:
《HF 设计模式》 C6 适配器模式&外观模式
打开FocusListener接口:
《HF 设计模式》 C6 适配器模式&外观模式
这就是适配器模式,FocusAdapter就是一个适配器,它实现了FocusListener接口

这里FocusAdapter实现了FocusListener,但是对于要完成的两个方法,选择了什么都不做,空适配器是合理的,这里将具体的实现方式交给用户,让用户选择重写FocusAdapter中的方法来使用

1.2 更换电视机

先来看一个简单的例子:

/**
 * @author 雫
 * @date 2021/3/5 - 9:25
 * @function 电视机
 */
public class Tv {

    public Tv() {}
    
    public void on() {
        System.out.println("关闭电视");
    }

    public void off() {
        System.out.println("打开电视");
    }
}

/**
 * @author 雫
 * @date 2021/3/5 - 8:19
 * @function 用户
 */
public class User {

    public static void main(String[] args) {
        Tv tv = new Tv();

        tv.on();
        tv.off();
    }
}

这个程序简单模拟用户使用电视机的过程,但是问题也很明显:
1,实现与功能捆绑,面向实现编程,而不是面向接口编程
2,存在对象依赖,应该依赖抽象,而不是具体类

我们用适配器模式扩展它的功能,并实现解耦:

/**
 * @author 雫
 * @date 2021/3/5 - 10:28
 * @function 电视
 */
public class Tv {
    public Tv() {}

    public void on() {
        System.out.println("打开电视");
    }

    public void off() {
        System.out.println("关闭电视");
    }

    public void watchMovie() {
        System.out.println("打开观影功能");
    }

    public void listenMusic() {
        System.out.println("打开音乐功能");
    }

    public void playGame() {
        System.out.println("打开游戏功能");
    }
}

/**
 * @author 雫
 * @date 2021/3/5 - 9:30
 * @function 电视功能接口
 */
public interface TvFunction {

    void on();

    void off();

    void watchMovie();

    void listenMusic();

    void playGame();
}

/**
 * @author 雫
 * @date 2021/3/5 - 9:31
 * @function 普通电视接口
 */
public class TvFunctionAdapter implements TvFunction {
    private Tv tv;

    public TvFunctionAdapter() {
        this.tv = new Tv();
    }

    @Override
    public void on() {
        tv.on();
    }

    @Override
    public void off() {
        tv.off();
    }

    @Override
    public void watchMovie() {
        System.out.println("请购买会员后享使用电影服务");
    }

    @Override
    public void listenMusic() {
        System.out.println("请购买会员后使用音乐服务");
    }

    @Override
    public void playGame() {
        System.out.println("请购买会员后使用游戏服务");
    }

}

/**
 * @author 雫
 * @date 2021/3/5 - 9:32
 * @function Vip电视接口
 */
public class TvFunctionVipAdapter implements TvFunction {
    private Tv tv;

    public TvFunctionVipAdapter() {
        this.tv = new Tv();
    }

    @Override
    public void on() {
        tv.on();
    }

    @Override
    public void off() {
        tv.off();
    }

    @Override
    public void watchMovie() {
        tv.watchMovie();
    }

    @Override
    public void listenMusic() {
        tv.listenMusic();
    }

    @Override
    public void playGame() {
        tv.playGame();
    }

}

/**
 * @author 雫
 * @date 2021/3/5 - 8:19
 * @function 用户
 */
public class User {
    private TvFunction tv;

    public User(TvFunction tv) {
        this.tv = tv;
    }

    public void setTv(TvFunction tv) {
        this.tv = tv;
    }

    public void openTv() {
        this.tv.on();
    }

    public void closeTv() {
        this.tv.off();
    }

    public void watchMovie() {
        this.tv.watchMovie();
    }

    public void ListenMusic() {
        this.tv.listenMusic();
    }

    public void platGame() {
        this.tv.playGame();
    }

}

测试:
《HF 设计模式》 C6 适配器模式&外观模式

通过使用适配器模式,我们将功能与实现解耦让User类只依赖抽象如果想要更改功能,只需要更改适配器即可
并且这里通过组合,将TvFunction作为User的成员,扩展了User的功能还提供了setTv()的方法,可以让用户替换适配器

1.3 适配器模式

适配器模式

将一个类的接口,转换成客户期望的另一个接口
适配器让原本接口不兼容的类可以轻松合作

1.2代码的工作方式:
《HF 设计模式》 C6 适配器模式&外观模式
1.2代码中:

Tv: 被适配者

TvFunctionVipAdapter/TvFunctionAdapter: 适配器

TvFunction: 目标接口

User: 客户

在客户中调用的方法,实际上是调用适配器中的方法
而适配器可以委托被适配者完成该方法,也可以自己完成该方法

《HF 设计模式》 C6 适配器模式&外观模式

1.4 枚举器与迭代器

在早期Java的集合类型(Vector,Stack,Hashtable)都实现了一个名为elements()的方法,该方法会返回一个枚举(Enumeration),这个Enumeration接口可以逐一遍历集合中的每个元素,而无需知道它们在几个中是如何被管理的
《HF 设计模式》 C6 适配器模式&外观模式
Enumeration中有两个方法,一个判断是否还有下一个元素,一个取得集合中的下一个元素

当sun推出更新的集合后,开始使用迭代器(Iterator)接口,这个接口和枚举很像,但是还提供了删除元素的能力
《HF 设计模式》 C6 适配器模式&外观模式
如果我们要维护一个老项目,这个项目中使用的都是枚举,但是现在我们希望新代码中对集合的操作全部采用迭代器来进行,为了让旧代码中的枚举变得像新代码中的迭代器,我们就需要为枚举制作一个适配器

枚举中的两个方法,迭代器中都有,但是迭代器有删除元素的方法,枚举没有,这时可以选择抛出异常并在文档中说明即可:

/**
 * @author 雫
 * @date 2021/3/5 - 11:24
 * @function 
 */
public class EnumeratorIterator implements Iterator {
    private Enumeration enumeration;

    public EnumeratorIterator(Enumeration enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    @Override
    public Object next() {
        return enumeration.nextElement();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

1.5 适配器模式小结

《HF 设计模式》 C6 适配器模式&外观模式

1,当需要使用一个现有的类而其接口不符合需要时,使用适配器

2,适配器改变接口以符合客户的期望

3,实现一个适配器根据目标接口的大小和复杂度而定

2 外观模式

2.1 模拟家庭影院

我们现在要自己搭建一个家庭影院,整个系统需要一些设备:
DVD播放机,投影机,自动屏幕,音响,爆米花机,空调…
《HF 设计模式》 C6 适配器模式&外观模式
我们来粗糙的模拟一下这个系统:

设备:

/**
 * @author 雫
 * @date 2021/3/5 - 12:18
 * @function 音响
 */
public class Amp {

    public Amp() {}

    public void on() {
        System.out.println("打开音响");
    }

    public void setDvd(Dvd dvd) {
        System.out.println("已设置声源为" + dvd);
    }

    public void setSurroundSound() {
        System.out.println("设置为环绕声");
    }

    public void setVolume(int volume) {
        System.out.println("已设置音量为" + volume);
    }


    public void off() {
        System.out.println("关闭音响");
    }
}

/**
 * @author 雫
 * @date 2021/3/5 - 12:16
 * @function DVD
 */
public class Dvd {
    private String name;

    public Dvd(String name) {
        this.name = name;
    }

    public void on() {
        System.out.println("打开DVD");
    }

    public void play(Movie movie) {
        System.out.println("播放" + movie);
    }

    public void off() {
        System.out.println("关闭DVD");
    }

    @Override
    public String toString() {
        return name;
    }
}
...

现在有了这些设备,我们可以看一场电影了,只不过在看电影前和看电影后需要做一些事:

/**
 * @author 雫
 * @date 2021/3/5 - 11:58
 * @function 看一场电影
 */
public class Test {

    public static void main(String[] args) {
        Amp amp = new Amp();
        Dvd dvd = new Dvd("DVD1");
        Light light = new Light();
        Movie movie = new Movie("xxx");
        Popper popper = new Popper();
        Projector projector = new Projector();
        Screen screen = new Screen();

        /*准备看电影*/
        popper.on();
        popper.pop();

        light.on();
        light.changeBright(10);

        screen.on();

        projector.on();
        projector.setInput(dvd);
        projector.wideScreenMode();

        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(20);

        dvd.on();
        dvd.play(movie);

        /*正在看电影*/

        /*电影结束*/
        popper.off();
        screen.off();
        amp.off();
        projector.off();
        dvd.off();
        light.off();

    }
}

好了,现在我们的用户想看一场电影,除了有全套的设备外,还需要走来走去前后执行20个操作,我们需要一个类似遥控器的工具,能一键完成看电影前的准备和看电影后的收尾操作

遥控器,执行一组操作,这自然会想到命令模式,采用宏命令来执行一组操作,但是这里有更简单更适合的模式,我们把系统整合到一起

2.2 整合家庭影院系统

我们把所有的设备实体放到另一个类(外观)中,通过组合来完成“一个操作调用一组操作”

《HF 设计模式》 C6 适配器模式&外观模式
整合系统后的HomeTheater:

/**
 * @author 雫
 * @date 2021/3/5 - 12:43
 * @function 家庭影院/外观
 */
public class HomeTheater {
    private Amp amp;
    private Dvd dvd;
    private Light light;
    private Movie movie;
    private Popper popper;
    private Projector projector;
    private Screen screen;

    public HomeTheater(Amp amp, Dvd dvd, Light light, Movie movie,
                Popper popper, Projector projector, Screen screen) {
        this.amp = amp;
        this.dvd = dvd;
        this.light = light;
        this.movie = movie;
        this.popper = popper;
        this.projector = projector;
        this.screen = screen;
    }

    public void startWatch() {
        this.popper.on();
        this.popper.pop();

        this.light.on();
        this.light.changeBright(10);

        this.screen.on();

        this.projector.on();
        this.projector.setInput(this.dvd);
        this.projector.wideScreenMode();

        this.amp.on();
        this.amp.setDvd(this.dvd);
        this.amp.setSurroundSound();
        this.amp.setVolume(20);

        this.dvd.on();
        this.dvd.play(movie);
    }

    public void overWatch() {
        this.popper.off();
        this.screen.off();
        this.amp.off();
        this.projector.off();
        this.dvd.off();
        this.light.off();
    }
    
}

简化了用户的操作:

/**
 * @author 雫
 * @date 2021/3/5 - 11:58
 * @function 看一场电影
 */
public class Test {

    public static void main(String[] args) {
        Amp amp = new Amp();
        Dvd dvd = new Dvd("DVD1");
        Light light = new Light();
        Movie movie = new Movie("xxx");
        Popper popper = new Popper();
        Projector projector = new Projector();
        Screen screen = new Screen();

        /*准备看电影*/
        HomeTheater homeTheater = new HomeTheater(amp, dvd,
                light, movie, popper, projector, screen);

        homeTheater.startWatch();

        /*电影结束*/
        homeTheater.overWatch();

    }
    
}

2.3 外观模式

外观模式

提供了一个统一的接口,用来访问子系统中的一群接口
外观定义了一个高层接口,让子系统更易使用

2.2中的代码就是将整个影院子系统包装成了一个外观,使得客户从子系统中解耦出来,即客户不需要一个个设备进行操作,只需要对
外观(家庭影院遥控器) 来进行操作

《HF 设计模式》 C6 适配器模式&外观模式

如果客户想要使用子系统中的功能,如增大音响音量,也可以从外观中取得,只需要外观提供额外的方法即可:

public void addVolume(int volume) {
       this.amp.setVolume(volume);
}

外观模式并不同于命令模式,命令模式完成了调用者和接收者的完全解耦,而外观模式只是将所有的对象依赖抛给了外观,一旦子系统组件需要更改替换,外观也需要改变,外观并没有实现完全的解耦

2.4 “最少”知识原则

我们根据外观模式,引入新的设计原则:

设计原则:
不要让太多的类耦合在一起

不要让太多的类耦合在一起,免得修改系统中的一部分,会影响到其它部分,如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,需要花更多时间维护扩展

我们在2.2中创建的HomeTheater虽然将用户解耦了出来,但它本身却陷入了大量的对象依赖,为了减少这些对象依赖,应该针对抽象编程,而不是针对具体类编程,这里采用命令模式实现这个家庭影院可能会更好

比如我们要从气象站中取得温度,而温度是由气象站内的温度计对象获取的,不采用最少知识原则时:

Station station = new station();

public float getTemp() {
	Thermometer thermoeter = station.getThermometer();
	return thermometer.getTemperature();
}

采用最少知识原则,尽量减少依赖对象:

//在Station类中新增一个方法
public void getTemoerature() {
	return this.thermoeter.getTemperature();
}

Station station = new station();
public float getTemp() {
	return station.getTemperature();
}

最少知识原则可以结合依赖倒置原则,降低系统间的耦合,让每个类更“干净”,更健壮

但是采用这个原则会产生更多的“外观类”,这可能会倒置复杂度和开发时间增加,并降低运行时的性能

2.5 适配器模式&外观模式小结

1,当需要一个接口实现多种功能时,采用适配器

2,当需要降低某个类的对象依赖时,采用外观

3,适配器将一个对象包装起来以改变其接口,外观将一群对象包装起来简化接口(接口是抽象的,不一定是interface)

上一篇:Java多线程04


下一篇:苹果正在研发下一代 Apple TV 遥控器