初识设计模式——外观模式(Facade Pattern)

cuixiaogang

外观模式(Facade Pattern)属于结构型设计模式,它为子系统中的一组接口提供了一个统一的高层接口,使得子系统更容易使用。此模式通过创建一个外观类,将复杂的子系统封装起来,客户端只需与外观类交互,而无需了解子系统内部的具体实现。

外观模式的结构

  • 外观角色(Facade):这是外观模式的核心,它了解各个子系统的功能和职责,负责将客户端的请求委派给相应的子系统进行处理。外观角色通常会提供一个简单的接口,客户端只需要和外观角色进行交互,而不需要直接和各个子系统打交道。
  • 子系统角色(Subsystem):子系统是由多个具体的类或模块组成的,它们实现了具体的业务逻辑。每个子系统都有自己独立的功能,并且可以独立地被调用。子系统并不知道外观角色的存在,它们只负责完成自己的任务。

外观模式结构图

案例

场景分析

假设我们有一个复杂的多媒体系统,包含视频播放器、音频播放器和字幕显示模块,客户端可以通过一个外观类来操作这些模块。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
// 视频播放器类
class VideoPlayer {
public function playVideo() {
echo "Playing video...\n";
}
}

// 音频播放器类
class AudioPlayer {
public function playAudio() {
echo "Playing audio...\n";
}
}

// 字幕显示类
class SubtitleDisplayer {
public function displaySubtitles() {
echo "Displaying subtitles...\n";
}
}

// 外观类
class MultimediaFacade {
private $videoPlayer;
private $audioPlayer;
private $subtitleDisplayer;

public function __construct() {
$this->videoPlayer = new VideoPlayer();
$this->audioPlayer = new AudioPlayer();
$this->subtitleDisplayer = new SubtitleDisplayer();
}

public function playMovie() {
$this->videoPlayer->playVideo();
$this->audioPlayer->playAudio();
$this->subtitleDisplayer->displaySubtitles();
}
}

// 客户端代码
$facade = new MultimediaFacade();
$facade->playMovie();
?>

UML类图

外观模式的优缺点

优点

  • 简化接口:客户端只需与外观类交互,无需了解子系统的复杂细节,降低了客户端的使用难度。
  • 解耦客户端与子系统:客户端与子系统之间的依赖关系被外观类隔离,子系统的变化不会直接影响客户端。
  • 提高可维护性:子系统的修改和扩展可以在不影响客户端的情况下进行,便于系统的维护和升级。

缺点

  • 不符合开闭原则:如果需要对外观类进行修改或扩展,可能会影响到已有的客户端代码。
  • 外观类可能变得复杂:随着子系统的不断增加和功能的不断扩展,外观类可能会变得越来越庞大,难以维护。

常见应用场景

  • 简化复杂系统的使用:当一个系统包含多个复杂的子系统时,可以使用外观模式为其提供一个简单的接口,方便客户端使用。
  • 封装旧系统:在对旧系统进行改造时,可以使用外观模式将旧系统的复杂接口封装起来,为新系统提供一个统一的接口。
  • 分层架构:在分层架构中,外观模式可以作为层与层之间的接口,简化层与层之间的交互。

不适用的应用场景

系统规模较小

当系统本身规模不大,子系统数量较少且逻辑简单时,使用外观模式可能会带来额外的复杂度。因为外观模式的主要目的是封装复杂子系统,若系统本身不复杂,创建外观类会增加不必要的代码量和设计复杂度。

例如,一个简单的控制台程序仅包含两个简单的功能类,一个用于数据读取,另一个用于数据处理。此时,直接在客户端代码中调用这两个类的方法会更加简洁,引入外观模式会使代码变得冗余。

需要细粒度控制

如果客户端需要对子系统进行细粒度的控制,外观模式就不太适用。外观模式提供的是一个统一的高层接口,隐藏了子系统的具体实现细节,这会限制客户端对子系统的直接操作。

比如在一个图形编辑软件中,客户端可能需要精确控制每一个绘图工具的属性和行为,如画笔的颜色、线条粗细、绘图模式等。若使用外观模式,将这些绘图工具封装在一个外观类中,客户端就无法直接对子系统(绘图工具)进行细致的调整。

频繁变动的子系统接口

当子系统的接口频繁变动时,使用外观模式可能会增加维护成本。外观模式在子系统和客户端之间建立了一层封装,子系统接口的变化可能需要同时修改外观类的实现,以保证外观类的接口与子系统的接口保持一致。

例如,一个电商系统的库存管理子系统,由于业务需求的变化,其接口可能会频繁更新,如增加新的库存查询条件、修改库存更新逻辑等。若使用外观模式,每次子系统接口变动都需要修改外观类,这会使代码的维护变得困难。

对性能要求极高的场景

在对性能要求极高的场景下,外观模式可能会引入额外的性能开销。因为外观模式需要通过外观类来间接调用子系统的方法,这会增加方法调用的层次和时间开销。

例如,在一个实时游戏系统中,对响应时间和性能要求非常高,每一个操作都需要在极短的时间内完成。此时,如果使用外观模式来封装游戏中的各种功能模块,可能会因为额外的方法调用而导致性能下降,影响游戏的流畅度。

外观模式在Thinkphp框架中的应用分析

ThinkPHP 5.1 采用了 MVC(Model-View-Controller)设计架构,并且在一定程度上运用了外观模式的思想,下面从不同方面来进行分析:

外观模式思想体现

1. 核心类的封装

ThinkPHP 5.1 把一些核心功能封装在外观类中,以此简化开发者对这些功能的使用。例如,数据库操作相关的功能被封装在 Db 类里,开发者借助这个外观类就能方便地进行数据库操作,而无需了解底层数据库连接、查询构建等复杂细节。

1
2
3
4
// 使用 Db 外观类进行数据库查询
use think\facade\Db;

$result = Db::name('user')->where('id', 1)->find();

在这个例子中,Db 类就是一个外观类,它为数据库操作提供了统一的高层接口,开发者只需调用 Db 类的方法,不用关心数据库连接、SQL 语句生成等具体实现。

2. 服务容器的使用

ThinkPHP 5.1 的服务容器也有外观模式的影子。服务容器会把各种服务注册到容器中,开发者可以通过外观类来访问这些服务。比如,日志服务可以通过 Log 外观类来使用。

1
2
3
4
// 使用 Log 外观类记录日志
use think\facade\Log;

Log::info('This is an info log');

Log 外观类为日志服务提供了简单的接口,开发者不需要了解日志服务的具体实现和配置,只需调用 Log 类的方法就能完成日志记录操作。

与传统外观模式的差异

1. 职责划分不同

传统的外观模式主要是为了简化复杂子系统的接口,而 ThinkPHP 5.1 的 MVC 架构更侧重于实现业务逻辑、数据和视图的分离。外观模式只是其中一部分功能的实现方式,并非整个架构的核心。

2. 灵活性和扩展性

ThinkPHP 5.1 的 MVC 架构具有更高的灵活性和扩展性。除了使用外观类,开发者还能直接使用底层的类和方法,以满足不同的业务需求。例如,开发者可以不使用 Db 外观类,而是直接实例化数据库连接类进行数据库操作。

1
2
3
4
5
// 直接实例化数据库连接类进行操作
use think\DbManager;

$db = DbManager::connect();
$result = $db->name('user')->where('id', 1)->find();

综上所述,ThinkPHP 5.1 的 MVC 架构在部分功能上运用了外观模式的思想,通过外观类简化了开发者对核心功能的使用,但它与传统的外观模式在职责划分和灵活性等方面存在差异。