3

前言

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

适配器模式是将一个类的接口转换成客户希望的另外一个接口,身边很多东西都是适用于适配器模式的,笔记本的电源(也叫电源适配器),是将220V的交流电转换为笔记本电脑所需要的12V(电流先忽略),笔记本电脑的各种接口,VGA转Hdml,USB-TypeA 转 USB-TypeC,亦或者你在香港买了个手机,充电器是你生活中没见过的三孔插座通过一个转换头转换为国内常用的插头,很多例子都能很形象的解释这个设计模式。适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。

定义适配器模式

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

对象和类适配器的类图

实际上有两种适配模式,"对象"适配器"类"适配器,在Java中类适配器不能实现,因为需要多重继承的支持。

  • "对象"适配器

  • "类"适配器

"对象"适配器是通过使用对象的组合实现的接口转换,"类"适配器则是同时继承被适配者和目标类实现的。

实现对象适配器

先来个简单的实现,现在我有一个鸭子类,它的子类都可以呱呱叫,我想让一个咯咯叫的火鸡也拥有鸭子的行为。我们可以使用对象适配器伪装火鸡让它看起来像鸭子。

鸭子行为类:Duck接口

public interface Duck {
    void quack(); // 呱呱叫
    void fly();
}

绿头鸭是鸭子的子类:MallardDuck

// 绿头鸭
public class MallardDuck implements Duck {
    public void quack() { // 呱呱叫
        System.out.println("Quack...");
    }
    public void fly() {
        System.out.println("I'm flying 5 meters");
    }
}

火鸡行为类:Turkey接口

public interface Turkey {
    void gobble(); // 咯咯叫
    void fly();
}

野火鸡是火鸡的子类:WildTurkey

public class WildTurkey implements Turkey {
    public void gobble() { // 咯咯叫
        System.out.println("Gobble...");
    }
    public void fly() {
        System.out.println("I'm flying a 1 meter");
    }
}

接下来我们需要让火鸡伪装成鸭子,写一个适配器,让它适配火鸡

火鸡适配器类:TurkeyAdapter

public class TurkeyAdapter implements Duck {
    Turkey turkey; // 组合
    // 取得要适配的对象引用
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    // 接口装换
    public void quack() { // 简单的转换
        turkey.gobble();
    }
    public void fly() { // 稍微难一点的转换
        for(int i = 0; i < 5; i++)
            turkey.fly();
    }
}

测试适配器:DuckTestDriver

public class DuckTestDriver {
    public static void main(String[] args) {
        // 绿头鸭子
        Duck duck = new MallardDuck(); 
        // 野火鸡
        Turkey turkey = new WildTurkey();
        // 野火鸡使用适配器伪装成鸭子
        TurkeyAdapter turkeyAdapter = 
            new TurkeyAdapter(turkey);

        System.out.println("The Turkey says...");
        turkey.gobble();
        turkey.fly();

        System.out.println("\nThe Duck says...");
        TestDuck(duck);

        System.out.println("\nThe TurkeyAdapter says...");
        TestDuck(turkeyAdapter);
    }
    static void TestDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

测试结果

The Turkey says...
Gobble...
I'm flying a 1 meter

The Duck says...
Quack...
I'm flying 5 meters

The TurkeyAdapter says...
Gobble...
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter

可见,火鸡在适配器的帮助下成功的被认为是鸭子,但是它的本质不变它还是火鸡,只能咯咯叫。

真实世界的适配器

旧世界的迭代器 和 新世界的迭代器

  • 在Java早期的集合(Collection)类型(例如:VectorStackHashTable这些类现在都被遗弃了)都实现了一个名为elements()方法。它会返回一个Enumeration举,跟我们现在的Iterator迭代器作用差不多,就是遍历集合,但是枚举是"只读"的,意味着它不能像迭代器那样删除元素

面对以前的遗留代码,这些代码只暴露出枚举器的接口,但是我们希望用使用迭代器,那么我们需要构造一个适配器让枚举器看起来像迭代器,进而使用它。

根据上例所了解的对象适配器我们先设计出类图,像这样:

编写一个EnumerationIterator适配器:

//使用了Iterator伪装的一个Enumeration
public class EnumerationIterator implements Iterator<Object> {
    Enumeration<?> enumeration;
    public EnumerationIterator(Enumeration<?> enumeration) {
        this.enumeration = enumeration;
    }
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }
    public Object next() {
        return enumeration.nextElement();
    }
    public void remove() { // 未获支持的操作
        throw new UnsupportedOperationException();
    }
}

可以看见适配器适配得并不完美,但这没有办法,毕竟Enumeration不是一个Iterator,只能选择在remove()方法直接抛出了一个未获支持操作异常UnsupportedOperationException,做出一个文档说明,让客户在使用的时候小心就没有太大问题。

测试一下:EnumerationIteratorTestDriver

public class EnumerationIteratorTestDriver {
    public static void main(String[] args) {
        Vector<Integer> v = new Vector<Integer>(
            Arrays.asList(1, 2, 3, 4, 5));
        // 使用了Iterator伪装的一个Enumeration
        Iterator<?> iterator = new EnumerationIterator(v.elements());
        while(iterator.hasNext())
            System.out.print(iterator.next());
    }
}

测试结果:

12345

与外观和装饰器混淆

简而言之:
适配器模式职责是 转换接口
外观模式职责是 简化接口
装饰器模式职责是 扩展对象的行为与责任

适配器的扩展应用

双向适配器,支持两边的接口,想要创建这样的适配器,必须实现涉及的两个接口。

1.3的例子中,如果来了一个嘎嘎叫的天鹅,我想让火鸡也伪装成它,可以实现一个双向的适配器去适配火鸡,让火鸡可以被看作是鸭子和鹅,像这样:

public interface Goose {
    gaggle(); // 嘎嘎叫
    gFly();
}
public class TwoWayAdapter implements Duck, Goose {
    Turkey turkey; // 组合
    Random rand;
    // 取得要适配的对象引用
    public TwoWayAdapter(Turkey turkey) {
        this.turkey = turkey;
        rand = new Random();
    }
    // 接口装换
    public void quack() { 
        turkey.gobble();
    }
    public void gaggle() {
        turkey.gobble();
    }
    public void fly() { // 模拟鸭子的飞行距离
        for(int i = 0; i < 5; i++)
            turkey.fly();
    }
    public void gFly() { // 鹅的飞行距离比火鸡短    
        for(rand.nextInt(5) == 0) // 五分之一
            turkey.fly();
    }
}

定义外观模式

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

外观模式类图

Cilent: 有了外观,客户只需要跟外观打交道,工作变得简单了,客户与子系统解耦了。
Facade:外观统一了接口
ComplacatedSubsystem:复杂的子系统

现在还是有点迷糊,通过接下来的例子更深一步认识外观模式

实现外观类

我组装了一个家庭游戏空间的系统,内含Xbox游戏机,超大屏电视,环绕立体声,空调,灯光。

看看这些组件的类图:

当我需要启动我的游戏空间系统时,我会这么做,先开灯,然后打开电视,然后。。。

light.on(); // 开灯
light.dim(10); // 亮度降低10%
airConditioner.on();// 开空调,默认20度
tv.on(); // 开电视    
tv.setVolume(11); // 设成最大音量11
StereoSpeaker speaker = tv.getStereoSpeaker();
speaker.setSurround(); // 设为环绕音
xbox.on(); // 打开Xbox游戏主机
xbox.setGame(game); // 选择游戏

用完了我还得需要反向地执行关闭动作,虽然该系统功能性很强但是使用非常麻烦,难道不能一键开启关闭吗?能!

我们可以实现一个提供更合理的接口的外观类,将这个复杂的系统变得容易使用

下面是实现了外观类的家庭游戏空间的系统:

外观类FamilyPlayPlaceFacade 只暴露出几个简单的方法,它将家庭游戏空间多个组件视为一个子系统Subsystem,通过调用这个子系统来实现playGames()方法,而且并未将子系统的类阻隔起来,我还是随时可以使用原来的子系统。

构造一个家庭游戏空间的外观类:FamilyPlayPlaceFacade

// 使用外观封装特定的一组行为,但不阻隔子系统
public class FamilyPlayPlaceFacade {
    // 使用对象组合
    Light light;
    AirConditioner airConditioner;
    TV tv;
    Xbox xbox;
    // 在构造器初始化对象
    public FamilyPlayPlaceFacade(
        Light light,
        AirConditioner airConditioner,
        TV tv, 
        Xbox xbox) {
        this.light = light;
        this.airConditioner = airConditioner;
        this.tv = tv;
        this.xbox = xbox;
    }
    // 设定特定一系列的操作
    public void playGames(String game) {
        System.out.println("Get ready to play a game...");
        light.on();
        light.dim(10); // 亮度降低10%
        airConditioner.on(); // 默认20度
        tv.on(); // 先开电视    
        tv.setVolume(11); // 设成最大音量11
        StereoSpeaker speaker = tv.getStereoSpeaker();
        speaker.setSurround(); // 设为环绕音
        xbox.on();
        xbox.setGame(game); // 选择游戏
        System.out.println();
    }
    // 负责关闭一切
    public void endGames() {
        System.out.println("Shutting game down...");
        xbox.off();
        tv.off();
        airConditioner.off();
        light.off();
    }
}

当然每个任务的细节都委托相应的组件处理,毕竟我只是个外观而已。

现在我终于可以一步到位了,来肝一把《怪物猎人:世界》,测试一下:

public class FamilyPlayPlaceDrive {
    public static void main(String[] args) {
        String location = "Living Room";
        // 实例化组件
        Light light = new Light(location);
        AirConditioner airConditioner = new AirConditioner(location);
        StereoSpeaker stereoSpeaker = new StereoSpeaker(location);
        TV tv = new TV(location, stereoSpeaker);
        Xbox xbox = new Xbox(location);
        // 实例化外观
        FamilyPlayPlaceFacade gameFacade = 
            new FamilyPlayPlaceFacade(light, airConditioner, tv, xbox);
        gameFacade.playGames("Monster Hunter:World");
        gameFacade.endGames();
    }
}

原则定义

莫忒耳法则(Law of Demeter)指的也是最少知识原则

最少知识原则:只和你的密友谈话

不要让太多的类耦合到一起,免得修改系统的一部分,会影响其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,也会因为太复杂而容易被他人了解。

指导方针

在该对象的方法内,我们只调用属于以下范围的方法:

  • 该对象本身
  • 被当做方法的参数而传递进来的对象
  • 此方法所创建或实例化的任何对象
  • 对象的任何组件

当遵守该原则对你的程序百利而无一害时,那就尽量去遵守它。

下面是四条指导方针的应用

public class Car {
    Engine engine; // 本类组件engine
    // 其他组件
    public Car() {
        // 初始化发动机
    }
    // 被当做参数传进来的对象key
    public void start(Key key) {
        // 方法内创建的对象doors
        Doors doors = new Doors();
        // 被当做参数传进来的对象key的方法
        boolean authorized = key.turns();
        if(authorized) {
            // 本类组件engine的方法
            engine.start();
            // 该对象本身Car的方法
            updateDashboardDisplay();
            // 方法内创建的对象door的方法
            doors.lock();
        }
    }
    public void updateDashboardDisplay() {
        // 显示更新
    }
}

缺点

虽然减少了对象之间的依赖减少了软件的维护成本

但是也会导致过多的"包装"类被制造出来,以处理和其他组件的沟通,这可能会导致复杂度开发时间的增加,并降低运行时的性能

要点

  • 当需要使用一个现有的类而其他接口不符合你的需求时,就使用适配器
  • 当需要简化接口并统一一个很大的接口或者一群复杂的接口时,使用外观
  • 适配器改变接口以符合客户的期望
  • 外观将客户从一个复杂的子系统中解耦
  • 实现一个适配器的工作量根据目标接口的大小与复杂度规定
  • 实现一个外观,需要将子系统组合进外观,然后将工作委托给子系统去执行
  • 适配器模式的两种形式:对象适配器 和 类适配器。类适配器需要用到多重继承
  • 可以为一个子系统实现一个以上的外观
  • 适配器将一个对象包装起来以改变接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象"包装"起来以简化接口

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!


前程有光
936 声望618 粉丝