设计模式之外观模式

引言

计算机启动过程涉及到多个硬件组件和软件系统的协同工作。从最初的 BIOS(基本输入输出系统)进行自检,确保硬件设备正常运行;到 CPU(中央处理器)的初始化,为后续的计算任务做好准备;再到内存的分配与管理,保证数据的高效存储和读取;还有硬盘中操作系统的加载,以及各种驱动程序的启动…… 每一个环节都至关重要,任何一个步骤出现问题,都可能导致计算机无法正常启动。

想象一下,如果每次启动计算机,我们都需要手动去控制每一个硬件组件的启动顺序,精确地设置每一个软件系统的参数,那将会是多么繁琐和复杂的一件事情!而**外观模式(Facade)**则为我们提供了一个可行的设计理念,我们只需要通过按下电源按钮这一简单的操作,就可以触发一系列复杂的启动流程,让计算机自动完成各个组件和系统的初始化工作。

概念

定义

外观模式(Facade Pattern) 是一种结构型设计模式。它为子系统中的一组接口提供了一个统一的高层接口,这个接口使得子系统更容易被使用。就像是为复杂的子系统搭建了一个简单的 “门面”,用户只需要与这个门面进行交互,而不需要了解子系统内部的复杂结构和交互细节。

结构

  • 外观(Facade):这是外观模式的核心角色,它知道哪些子系统类负责处理请求,将客户端的请求代理给适当的子系统对象。它提供了一个简单统一的接口给客户端,隐藏了子系统的复杂性。
  • 子系统(Subsystem):可以有多个子系统类,每个子系统类都实现了一些特定的功能,它们相互协作来完成复杂的任务。但这些子系统类对于客户端来说是不直接可见的,它们只与外观角色进行交互。
  • 客户端(Client):使用外观模式的一方,它只需要与外观角色进行交互,而不需要了解子系统的内部结构和实现细节。客户端通过调用外观角色提供的统一接口来完成所需的操作,降低了客户端的使用难度。
    在这里插入图片描述

交互机制

外观模式的交互过程遵循以下步骤:

  1. 客户端发起请求:客户端需要完成某个任务时,会调用外观角色提供的统一接口。
  2. 外观角色接收请求:外观角色接收到客户端的请求后,根据请求的类型和内容,将其转发给相应的子系统对象。
  3. 子系统执行操作:子系统对象接收到外观角色传递的请求后,执行具体的业务逻辑,并将执行结果返回给外观角色。
  4. 外观角色返回结果:外观角色将子系统返回的结果进行整理和处理,然后将最终结果返回给客户端。

设计原则

外观模式遵循了一些重要的设计原则,这些原则是其底层原理的重要支撑:

  • 迪米特法则(最少知识原则):该法则强调一个对象应该对其他对象有最少的了解。在外观模式中,客户端只与外观角色进行交互,而不需要与多个子系统类直接通信,减少了客户端与子系统之间的耦合度。
  • 单一职责原则:每个子系统类只负责完成自己特定的任务,具有单一的职责。外观角色则负责协调子系统之间的交互,为客户端提供统一的接口,也具有明确的职责。这种职责的划分使得系统的各个部分功能清晰,便于维护和扩展。

示例

下面我们使用引言种的例子编写示例代码。

类图

在这里插入图片描述

C++实现

#include <iostream>

// 子系统类:BIOS
class BIOS {
public:
    void powerOnSelfTest() {
        std::cout << "BIOS: Performing Power-On Self-Test (POST)" << std::endl;
    }
    void initializeHardware() {
        std::cout << "BIOS: Initializing hardware components" << std::endl;
    }
};

// 子系统类:CPU
class CPU {
public:
    void initialize() {
        std::cout << "CPU: Initializing the central processing unit" << std::endl;
    }
};

// 子系统类:内存
class Memory {
public:
    void initialize() {
        std::cout << "Memory: Initializing the memory" << std::endl;
    }
};

// 子系统类:硬盘
class HardDrive {
public:
    void loadOperatingSystem() {
        std::cout << "Hard Drive: Loading the operating system" << std::endl;
    }
};

// 外观类:计算机启动外观
class ComputerFacade {
private:
    BIOS bios;
    CPU cpu;
    Memory memory;
    HardDrive hardDrive;

public:
    void startComputer() {
        // 调用 BIOS 的自检和初始化硬件功能
        bios.powerOnSelfTest();
        bios.initializeHardware();

        // 调用 CPU 的初始化功能
        cpu.initialize();

        // 调用内存的初始化功能
        memory.initialize();

        // 调用硬盘的加载操作系统功能
        hardDrive.loadOperatingSystem();

        std::cout << "Computer has been successfully started." << std::endl;
    }
};

// 客户端代码
int main() {
    ComputerFacade computer;
    // 调用外观类的启动计算机方法
    computer.startComputer();

    return 0;
}

Java实现

// 子系统类:BIOS
class BIOS {
    public void powerOnSelfTest() {
        System.out.println("BIOS: Performing Power-On Self-Test (POST)");
    }

    public void initializeHardware() {
        System.out.println("BIOS: Initializing hardware components");
    }
}

// 子系统类:CPU
class CPU {
    public void initialize() {
        System.out.println("CPU: Initializing the central processing unit");
    }
}

// 子系统类:内存
class Memory {
    public void initialize() {
        System.out.println("Memory: Initializing the memory");
    }
}

// 子系统类:硬盘
class HardDrive {
    public void loadOperatingSystem() {
        System.out.println("Hard Drive: Loading the operating system");
    }
}

// 外观类:计算机启动外观
class ComputerFacade {
    private BIOS bios;
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        // 初始化子系统
        bios = new BIOS();
        cpu = new CPU();
        memory = new Memory();
        hardDrive = new HardDrive();
    }

    public void startComputer() {
        // 调用 BIOS 的自检和初始化硬件功能
        bios.powerOnSelfTest();
        bios.initializeHardware();

        // 调用 CPU 的初始化功能
        cpu.initialize();

        // 调用内存的初始化功能
        memory.initialize();

        // 调用硬盘的加载操作系统功能
        hardDrive.loadOperatingSystem();

        System.out.println("Computer has been successfully started.");
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();
        // 调用外观类的启动计算机方法
        computer.startComputer();
    }
}

代码解释

  1. 子系统类
    • BIOS类:包含powerOnSelfTest()initializeHardware()方法,分别用于执行加电自检和初始化硬件组件。
    • CPU类:包含initialize()方法,用于初始化中央处理器。
    • Memory类:包含initialize()方法,用于初始化内存。
    • HardDrive类:包含loadOperatingSystem()方法,用于加载操作系统。
  2. 外观类
    • ComputerFacade类:包含了所有子系统类的实例。startComputer()方法封装了计算机启动的整个过程,按顺序调用了各个子系统类的方法。
  3. 客户端代码
    • main()函数中,创建了ComputerFacade类的对象,并调用其startComputer()方法。客户端只需要与外观类进行交互,无需了解各个子系统的具体实现细节。

外观模式的优缺点

优点

1. 简化接口

外观模式通过提供一个统一的接口,将子系统的复杂性隐藏起来。客户端只需与外观类进行交互,而无需了解子系统内部的具体实现和各个组件之间的交互细节。例如,在一个电商系统中,涉及库存管理、订单处理、支付等多个复杂子系统,使用外观模式可以提供一个简单的 “下单” 接口,客户端调用该接口即可完成一系列复杂操作,降低了客户端的使用难度。

2. 解耦客户端和子系统

外观模式使得客户端和子系统之间的耦合度降低。客户端不直接与子系统的各个组件交互,而是通过外观类间接访问。当子系统内部发生变化时,只要外观类的接口保持不变,客户端代码就无需修改。例如,在一个游戏开发中,游戏的音频、视频、物理引擎等是不同的子系统,通过外观模式可以将这些子系统与游戏主逻辑解耦,方便后续对子系统进行升级或替换。

3. 提高可维护性

由于子系统的复杂性被封装在外观类中,当需要对系统进行维护时,只需关注外观类和子系统的接口,而不需要深入了解整个子系统的实现细节。同时,当子系统发生变化时,只需要修改外观类中与该子系统相关的代码,不会影响到其他部分。例如,在一个企业级应用中,数据库访问、业务逻辑处理等是不同的子系统,通过外观模式可以提高系统的可维护性,降低维护成本。

4. 提高可扩展性

当需要添加新的子系统或功能时,只需要在外观类中添加相应的接口和逻辑,而不需要修改客户端代码。这使得系统具有更好的扩展性,能够适应不断变化的需求。例如,在一个在线教育系统中,原本只有课程展示和学习功能,后来需要添加考试功能,通过外观模式可以方便地在外观类中添加与考试功能相关的接口,而不影响现有的客户端代码。

5. 遵循设计原则

外观模式遵循了迪米特法则(最少知识原则),即一个对象应该对其他对象有最少的了解。客户端只与外观类进行交互,减少了与子系统中多个对象的直接通信,降低了系统的复杂性。

缺点

1. 限制子系统的灵活性

外观类提供了一个统一的接口,这可能会限制客户端对子系统的个性化使用。如果客户端需要使用子系统的某些特定功能,而这些功能没有在外观类的接口中提供,那么客户端就无法直接访问。例如,在一个图形处理系统中,外观类提供了一些常用的图形处理功能,但如果客户端需要使用一些特殊的算法,而外观类没有封装该算法,客户端就需要绕过外观类直接访问子系统,这就破坏了外观模式的封装性。

2. 外观类可能过于庞大

随着子系统功能的不断增加和扩展,外观类可能会变得越来越庞大,包含大量的方法和逻辑。这会导致外观类的代码变得复杂,难以维护和理解。例如,在一个大型的企业资源规划(ERP)系统中,涉及多个业务模块和子系统,外观类可能会包含几百个方法,这给代码的维护和扩展带来了很大的挑战。

3. 不符合开闭原则

如果需要对外观类的接口进行修改或扩展,可能会影响到客户端代码。因为客户端依赖于外观类的接口,当接口发生变化时,客户端可能需要进行相应的修改。这不符合开闭原则(对扩展开放,对修改关闭),在一定程度上降低了系统的可维护性和可扩展性。例如,在一个电商系统中,原本的外观类提供了 “下单” 接口,后来需要在该接口中添加一些额外的参数,这就可能需要修改客户端调用该接口的代码。

注意事项

设计外观类时

  • 合理封装子系统功能
    • 外观类应准确地将子系统中相关的、为实现特定业务功能而协同工作的操作封装起来。比如在一个多媒体播放系统中,外观类要把视频解码、音频解码、画面渲染、声音播放等子系统操作封装成一个完整的 “播放” 功能接口。不能随意拼凑子系统功能,导致接口语义不清晰。
    • 避免过度封装和封装不足。过度封装可能会把一些不需要暴露给客户端的细节也封装在外观类中,增加了外观类的复杂性;封装不足则无法为客户端提供足够简化的接口,失去了外观模式的意义。
  • 接口设计简洁明了
    • 外观类的接口应该简单易懂,符合客户端的使用习惯和业务需求。接口方法的命名要具有描述性,让客户端能够直观地知道该方法的作用。例如,在一个文件处理系统中,外观类提供的接口方法可以命名为openFilereadFilewriteFile等,方便客户端调用。
    • 控制接口的数量,避免提供过多的方法,导致客户端使用时感到困惑。如果有多个相关的操作,可以考虑将它们组合成一个复合操作,减少接口的复杂度。

处理子系统变化时

  • 保持接口稳定性
    • 当子系统内部发生变化时,要尽量保证外观类的接口不变。因为客户端是依赖于外观类的接口进行开发的,如果接口频繁变动,会导致客户端代码需要不断修改,增加了维护成本。例如,在一个电商系统中,库存管理子系统的实现方式发生了变化,但外观类的 “检查库存” 接口应该保持不变,客户端调用该接口的代码无需修改。
    • 如果确实需要对外观类的接口进行修改,要充分考虑对客户端的影响,并采取相应的兼容措施,如提供旧接口的过渡版本或给出明确的升级指南。
  • 考虑子系统的独立性
    • 外观模式虽然将子系统封装起来,但在设计时要考虑子系统的独立性,确保子系统之间的耦合度尽可能低。这样,当某个子系统发生变化时,不会对其他子系统和外观类造成过多的影响。例如,在一个游戏开发中,游戏的图形渲染子系统和音频处理子系统应该是相对独立的,外观类在封装它们的功能时,要避免引入不必要的耦合。

系统架构层面

  • 避免外观类成为 “上帝类”
    • 随着系统的发展和功能的增加,外观类可能会变得越来越庞大,包含过多的业务逻辑和子系统调用。要避免这种情况的发生,防止外观类成为一个无所不能的 “上帝类”。可以将一些复杂的业务逻辑拆分成多个小的外观类,或者采用分层架构,将不同层次的功能封装在不同的外观类中。例如,在一个大型企业级应用中,可以将前端展示层、业务逻辑层、数据访问层分别封装不同的外观类,降低单个外观类的复杂度。
  • 与其他设计模式结合使用
    • 外观模式可以与其他设计模式结合使用,以更好地满足系统的需求。例如,与工厂模式结合,通过工厂模式创建子系统对象,然后在外观类中使用这些对象,提高系统的可扩展性和可维护性;与单例模式结合,确保外观类只有一个实例,避免资源的浪费。

客户端使用方面

  • 明确使用场景

    • 客户端在使用外观类时,要明确外观模式的使用场景,避免在不需要的地方滥用。外观模式适用于简化复杂子系统的使用,如果子系统本身并不复杂,或者客户端只需要使用子系统的部分功能,可能不需要使用外观模式。
  • 不要绕过外观类

    • 客户端应该遵循外观模式的设计原则,通过外观类来访问子系统的功能,而不是绕过外观类直接与子系统进行交互。这样可以保证系统的封装性和可维护性。如果客户端绕过外观类直接访问子系统,当子系统发生变化时,客户端代码可能会受到影响,破坏了外观模式的解耦作用。

    在 C++ 中,我们可以使用私有构造函数和友元类来防止用户直接实例化子系统类。

应用场景

简化复杂系统的使用

  • 计算机系统启动:计算机启动过程涉及 BIOS 自检、CPU 初始化、内存加载、硬盘驱动挂载等一系列复杂操作。通过外观模式,可以提供一个简单的 “开机” 接口,用户只需按下电源按钮(调用外观接口),系统就能自动完成所有子系统的初始化和启动操作,而无需了解每个硬件组件和软件系统的具体启动步骤。
  • 家庭影院系统:家庭影院包含投影仪、音响、蓝光播放器、灯光等多个设备。使用外观模式可以创建一个统一的 “观看电影” 模式接口,用户只需按下一个按钮,外观类就能协调各个设备完成相应操作,如打开投影仪、调整画面、打开音响、设置音效、调暗灯光等,避免了用户分别操作每个设备的繁琐过程。

整合第三方库或框架

  • 图像处理:在开发图像处理应用时,可能会使用到多个不同的图像处理库,如 OpenCV、Pillow 等,这些库提供了丰富但复杂的功能接口。通过外观模式,可以创建一个统一的图像处理外观类,将不同库的功能进行封装和整合,为开发者提供简单易用的接口,如 “调整亮度”“裁剪图像”“添加水印” 等,降低了使用第三方库的难度。
  • 支付系统集成:在电商或在线支付应用中,需要集成多种支付方式,如支付宝、微信支付、银联支付等。每个支付方式的接口和处理流程都有所不同,使用外观模式可以创建一个统一的支付外观类,将不同支付方式的调用逻辑封装起来,开发者只需调用外观类的 “支付” 接口,就能完成支付操作,而无需关心具体使用的是哪种支付方式以及其复杂的交互流程。

分层架构中的通信

  • 企业级应用开发:在企业级应用的分层架构中,通常分为表示层、业务逻辑层、数据访问层等。外观模式可以在不同层次之间提供一个统一的接口,降低层与层之间的耦合度。例如,表示层只需与业务逻辑层的外观类进行交互,而无需直接调用业务逻辑层的多个具体业务类,这样当业务逻辑层的实现发生变化时,只要外观类的接口不变,表示层的代码就无需修改。
  • 微服务架构:在微服务架构中,一个业务功能可能由多个微服务协同完成。使用外观模式可以创建一个服务外观,将多个微服务的调用进行封装,为客户端提供一个统一的接口。客户端只需调用服务外观的接口,就能完成复杂的业务操作,而无需了解各个微服务的具体实现和调用方式,提高了系统的可维护性和可扩展性。

降低系统的耦合度

  • 游戏开发:游戏中包含多个子系统,如角色系统、战斗系统、物品系统、音效系统等。通过外观模式,可以为每个子系统创建一个外观类,将子系统的内部实现细节封装起来,其他子系统只需与外观类进行交互。这样,当某个子系统的实现发生变化时,不会影响到其他子系统,降低了系统的耦合度,提高了代码的可维护性和可扩展性。
  • 汽车制造:汽车是一个复杂的系统,包含发动机、变速箱、刹车系统、电气系统等多个子系统。在汽车的设计和生产过程中,可以使用外观模式为每个子系统提供一个统一的接口,整车的控制系统只需与各个子系统的外观类进行交互,而无需了解子系统的具体实现细节。这样,当某个子系统进行升级或改进时,不会对整车的控制系统产生影响,降低了系统的耦合度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值