前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面向对象设计的设计模式(七):外观模式

面向对象设计的设计模式(七):外观模式

作者头像
用户2932962
发布2019-03-18 14:51:38
8740
发布2019-03-18 14:51:38
举报
文章被收录于专栏:程序员维他命

定义

外观模式(Facade Pattern):外观模式定义了一个高层接口,为子系统中的一组接口提供一个统一的接口。外观模式又称为门面模式,它是一种结构型设计模式模式。

定义解读:通过这个高层接口,可以将客户端与子系统解耦:客户端可以不直接访问子系统,而是通过外观类间接地访问;同时也可以提高子系统的独立性和可移植性。

适用场景

  • 子系统随着业务复杂度的提升而变得越来越复杂,客户端需要某些子系统共同协作来完成某个任务。
  • 在多层结构的系统中,使用外观对象可以作为每层的入口来简化层间的调用。

成员与类图

成员

外观模式包括客户端共有三个成员:

  • 客户端类(Client):客户端是意图操作子系统的类,它与外观类直接接触;与外观类间接接触
  • 外观类(Facade):外观类知晓各个子系统的职责和接口,封装子系统的接口并提供给客户端
  • 子系统类(SubSystem):子系统类实现子系统的功能,对外观类一无所知

下面通过类图来看一下各个成员之间的关系:

模式类图

外观模式类图

上图中的method1&2()方法就是调用SubSystem1SubSystem2method1()method2()方法。同样适用于method2&3()

代码示例

场景概述

模拟一个智能家居系统。这个智能家居系统可以用一个中央遥控器操作其所接入的一些家具:台灯,音箱,空调等等。

在这里我们简单操纵几个设备:

  • 空调
  • CD Player
  • DVD Player
  • 音箱
  • 投影仪

场景分析

有的时候,我们需要某个设备可以一次执行两个不同的操作;也可能会需要多个设备共同协作来执行一些任务。比如:

假设我们可以用遥控器直接开启热风,那么实际上就是两个步骤:

  1. 开启空调
  2. 空调切换为热风模式

我们把这两个步骤用一个操作包含起来,一步到位。像这样简化操作步骤的场景比较适合用外观模式。

同样的,我们想听歌的话,需要四个步骤:

  1. 开启CD Player
  2. 开启音箱
  3. 连接CD Player和音箱
  4. 播放CD Player

这些步骤我们也可以装在单独的一个接口里面。

类似的,如果我们想看DVD的话,步骤会更多,因为DVD需要同时输出声音和影像:

  1. 开启DVD player
  2. 开启音箱
  3. 音响与DVD Player连接
  4. 开启投影仪
  5. 投影仪与DVD Player连接
  6. 播放DVD Player

这些接口也可以装在一个单独的接口里。

最后,如果我们要出门,需要关掉所有家用电器,也不需要一个一个将他们关掉,也只需要一个关掉的总接口就好了,因为这个关掉的总接口里面可以包含所有家用电器的关闭接口。

因此,这些设备可以看做是该智能家居系统的子系统;而这个遥控器则扮演的是外观类的角色。

下面我们用代码来看一下如何实现这些设计。

代码实现

因为所有家用电器都有开启和关闭的操作,所以我们先创建一个家用电器的基类HomeDevice

代码语言:javascript
复制
//================== HomeDevice.h ==================
//设备基类

@interface HomeDevice : NSObject

//连接电源
- (void)on;

//关闭电源
- (void)off;

@end

然后是继承它的所有家用电器类:

空调类AirConditioner:

代码语言:javascript
复制
//================== AirConditioner.h ==================

@interface AirConditioner : HomeDevice

//高温模式
- (void)startHighTemperatureMode;

//常温模式
- (void)startMiddleTemperatureMode;

//低温模式
- (void)startLowTemperatureMode;

@end

CD Player类:CDPlayer:

代码语言:javascript
复制
//================== CDPlayer.h ==================

@interface CDPlayer : HomeDevice

- (void)play;

@end

DVD Player类:DVDPlayer:

代码语言:javascript
复制
//================== DVDPlayer.h ==================

@interface DVDPlayer : HomeDevice

- (void)play;

@end

音箱类VoiceBox:

代码语言:javascript
复制
//================== VoiceBox.h ==================

@class CDPlayer;
@class DVDPlayer;

@interface VoiceBox : HomeDevice

//与CDPlayer连接
- (void)connetCDPlayer:(CDPlayer *)cdPlayer;

//与CDPlayer断开连接
- (void)disconnetCDPlayer:(CDPlayer *)cdPlayer;

//与DVD Player连接
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;

//与DVD Player断开连接
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;

@end

投影仪类Projecter

代码语言:javascript
复制
//================== Projecter.h ==================

@interface Projecter : HomeDevice

//与DVD Player连接
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;

//与DVD Player断开连接
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;

@end

注意,音箱是可以连接CD Player和DVD Player的;而投影仪只能连接DVD Player

现在我们把所有的家用电器类和他们的接口都定义好了,下面我们看一下该实例的外观类HomeDeviceManager如何设计。

首先我们看一下客户端期望外观类实现的接口:

代码语言:javascript
复制
//================== HomeDeviceManager.h ==================

@interface HomeDeviceManager : NSObject

//===== 关于空调的接口 =====

//空调吹冷风
- (void)coolWind;

//空调吹热风
- (void)warmWind;


//===== 关于CD Player的接口 =====

//播放CD
- (void)playMusic;

//关掉音乐
- (void)offMusic;


//===== 关于DVD Player的接口 =====

//播放DVD
- (void)playMovie;

//关闭DVD
- (void)offMoive;


//===== 关于总开关的接口 =====

//打开全部家用电器
- (void)allDeviceOn;

//关闭所有家用电器
- (void)allDeviceOff;

@end

上面的接口分为了四大类,分别是:

  • 关于空调的接口
  • 关于CD Player的接口
  • 关于DVD Player的接口
  • 关于总开关的接口

为了便于读者理解,这四类的接口所封装的子系统接口的数量是逐渐增多的。

在看这些接口时如何实现的之前,我们先看一下外观类是如何保留这些子系统类的实例的。在该代码示例中,这些子系统类的实例在外观类的构造方法里被创建,而且作为外观类的成员变量被保存了下来。

代码语言:javascript
复制
//================== HomeDeviceManager.m ==================

@implementation HomeDeviceManager
{
    NSMutableArray *_registeredDevices;//所有注册(被管理的)的家用电器
    AirConditioner *_airconditioner;
    CDPlayer *_cdPlayer;
    DVDPlayer *_dvdPlayer;
    VoiceBox *_voiceBox;
    Projecter *_projecter;

}

- (instancetype)init{

    self = [super init];

    if (self) {

        _airconditioner = [[AirConditioner alloc] init];
        _cdPlayer = [[CDPlayer alloc] init];
        _dvdPlayer = [[DVDPlayer alloc] init];
        _voiceBox = [[VoiceBox alloc] init];
        _projecter = [[Projecter alloc] init];

        _registeredDevices = [NSMutableArray arrayWithArray:@[_airconditioner,
                                                              _cdPlayer,
                                                              _dvdPlayer,
                                                              _voiceBox,
                                                              _projecter]];
    }
    return self;
}

其中 _registeredDevices这个成员变量是一个数组,它包含了所有和这个外观类实例关联的子系统实例。

子系统与外观类的关联实现方式不止一种,不作为本文研究重点,现在只需知道外观类保留了这些子系统的实例即可。按照顺序,我们首先看一下关于空调的接口的实现:

代码语言:javascript
复制
//================== HomeDeviceManager.m ==================

//空调吹冷风
- (void)coolWind{

    [_airconditioner on];
    [_airconditioner startLowTemperatureMode];

}

//空调吹热风
- (void)warmWind{

    [_airconditioner on];
    [_airconditioner startHighTemperatureMode];
}

吹冷风和吹热风的接口都包含了空调实例的两个接口,第一个都是开启空调,第二个则是对应的冷风和热风的接口。

我们接着看关于CD Player的接口的实现:

代码语言:javascript
复制
//================== HomeDeviceManager.m ==================

- (void)playMusic{

    //1. 开启CDPlayer开关
    [_cdPlayer on];

    //2. 开启音箱
    [_voiceBox on];

    //3. 音响与CDPlayer连接
    [_voiceBox connetCDPlayer:_cdPlayer];

    //4. 播放CDPlayer
    [_cdPlayer play];
}

//关掉音乐
- (void)offMusic{

   //1. 切掉与音箱的连接
    [_voiceBox disconnetCDPlayer:_cdPlayer];

    //2. 关掉音箱
    [_voiceBox off];

    //3. 关掉CDPlayer
    [_cdPlayer off];
}

在上面的场景分析中提到过,听音乐这个指令要分四个步骤:CD Player和音箱的开启,二者的连接,以及播放CD Player,这也比较符合实际生活中的场景。关掉音乐也是先断开连接再切断电源(虽然直接切断电源也可以)。

接下来我们看一下关于DVD Player的接口的实现:

代码语言:javascript
复制
//================== HomeDeviceManager.m ==================

- (void)playMovie{

    //1. 开启DVD player
    [_dvdPlayer on];

    //2. 开启音箱
    [_voiceBox on];

    //3. 音响与DVDPlayer连接
    [_voiceBox connetDVDPlayer:_dvdPlayer];

    //4. 开启投影仪
    [_projecter on];

    //5.投影仪与DVDPlayer连接
    [_projecter connetDVDPlayer:_dvdPlayer];

    //6. 播放DVDPlayer
    [_dvdPlayer play];
}


- (void)offMoive{

    //1. 切掉音箱与DVDPlayer连接
    [_voiceBox disconnetDVDPlayer:_dvdPlayer];

    //2. 关掉音箱
    [_voiceBox off];

    //3. 切掉投影仪与DVDPlayer连接
    [_projecter disconnetDVDPlayer:_dvdPlayer];

    //4. 关掉投影仪
    [_projecter off];

    //5. 关掉DVDPlayer
    [_dvdPlayer off];
}

因为DVD Player要同时连接音箱和投影仪,所以这两个接口封装的子系统接口相对于CD Player的更多一些。

最后我们看一下关于总开关的接口的实现:

代码语言:javascript
复制
//================== HomeDeviceManager.m ==================

//打开全部家用电器
- (void)allDeviceOn{

    [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
        [device on];
    }];
}


//关闭所有家用电器
- (void)allDeviceOff{

    [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
        [device off];
    }];
}

这两个接口是为了方便客户端开启和关闭所有设备的,有这两个接口的话,用户就不用一一开启或关闭多个设备了。

关于这两个接口的实现:

上文说过,该外观类通过一个数组成员变量_registeredDevices来保存所有可操作的设备。所以如果我们需要开启或关闭所有的设备就可以遍历这个数组并向每个元素调用onoff方法。因为这些元素都继承于HomeDevice,也就是都有onoff方法。

这样做的好处是,我们不需要单独列出所有设备来分别调用它们的接口;而且后面如果添加或者删除某些设备的话也不需要修改这两个接口的实现了。

下面我们看一下该demo多对应的类图。

代码对应的类图

外观模式代码示例类图

从上面的UML类图中可以看出,该示例的子系统之间的耦合还是比较多的;而外观类HomeDeviceManager的接口大大简化了User对这些子系统的使用成本。

优点

  • 实现了客户端与子系统间的解耦:客户端无需知道子系统的接口,简化了客户端调用子系统的调用过程,使得子系统使用起来更加容易。同时便于子系统的扩展和维护。
  • 符合迪米特法则(最少知道原则):子系统只需要将需要外部调用的接口暴露给外观类即可,而且他的接口则可以隐藏起来。

缺点

  • 违背了开闭原则:在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的代码。

Objective-C & Java 的实践

  • Objective-C:SDWebImage封装了负责图片下载的类和负责图片缓存的类,而外部仅向客户端暴露了简约的下载图片的接口。
  • Java:Spring-JDBC中的JdbcUtils封装了ConnectionResultSetStatement的方法提供给客户端
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员维他命 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 定义
  • 适用场景
  • 成员与类图
    • 成员
      • 模式类图
      • 代码示例
        • 场景概述
          • 场景分析
            • 代码实现
              • 代码对应的类图
              • 优点
              • 缺点
              • Objective-C & Java 的实践
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档