一、引言
在软件开发的世界,软件工程中,系统的复杂度常常像迷宫一样让开发者头疼不已。当多个子系统相互交织,各自有着复杂的接口和交互逻辑时,如何让客户端轻松地与这些子系统打交道就成了一个亟待解决的问题。这就如同在一个拥有众多电器设备(如电视、音响、灯光系统等)的智能家居环境中,用户不希望为了操作每个设备而学习不同的复杂操作方式。外观模式犹如一位贴心的管家,为我们解决了这个难题。
二、定义与描述
外观模式是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口,这个高层接口就像一道屏障,将子系统的复杂性隐藏起来,对外只暴露简单、统一的操作。客户端只需要与这个外观接口交互,而不必深入了解子系统内部各个接口的具体实现和交互关系。
三、抽象背景
随着软件系统的不断发展和功能扩充,系统内部会划分出多个子系统,每个子系统都为了特定的功能而设计了许多接口。例如在一个大型的企业级应用中,可能有财务子系统、库存子系统、人力资源子系统等。这些子系统各自的接口众多且复杂,如果让客户端(如业务逻辑层或者用户界面层)直接与这些子系统交互,客户端的代码将会变得臃肿不堪,而且一旦子系统内部接口发生变化,客户端代码就需要大量修改。
四、适用场景与现实问题解决
- 简化复杂系统访问:当一个系统包含许多复杂的子系统时,外观模式可以提供一个简单的入口。比如在一个电商系统中,订单处理、库存管理、支付处理等多个子系统协同工作。使用外观模式可以创建一个电商外观类,将这些子系统的操作封装起来,让外部调用者(如前端页面或者其他外部系统)只需要与这个电商外观类交互即可。
- 分层架构中的层间解耦:在分层架构(如三层架构中的表示层、业务逻辑层和数据访问层)中,业务逻辑层可以作为数据访问层的外观。表示层不需要了解数据访问层的具体数据库操作(如SQL语句的执行等),只需要调用业务逻辑层提供的简洁方法。这样一旦数据访问层的数据库结构或者操作方式发生改变,只要业务逻辑层的外观接口不变,就不会影响到表示层。
五、外观模式的现实生活的例子
- 家庭影院系统:一个家庭影院系统由多个设备组成,如DVD播放器、投影仪、音响等。每个设备都有自己的一套操作方法。我们可以创建一个家庭影院外观类,这个类包含了打开影院(依次打开DVD播放器、投影仪、音响并设置好相关参数)、播放影片(让DVD播放器播放影片并调整音响音量等)、关闭影院(关闭所有设备)等操作。用户只需要操作这个家庭影院外观类,而不需要分别去操作每个设备。
- 汽车启动系统:汽车内部有很多复杂的子系统,如发动机系统、电子控制系统、燃油系统等。当我们启动汽车时,我们不需要分别去启动每个子系统,只需要转动车钥匙(这个操作就相当于调用汽车启动系统这个外观类的启动方法),汽车启动系统内部会协调各个子系统完成启动操作。
六、初衷与问题解决
初衷是为了降低客户端与复杂子系统之间的耦合度,让客户端能够更轻松地使用子系统功能。通过外观模式,将子系统的复杂性封装在外观类内部,当子系统内部发生变化(如接口修改、内部逻辑调整)时,只要外观类提供的接口不变,客户端就不需要修改代码,从而提高了系统的可维护性和可扩展性。
七、代码示例
Java示例
类图:
HardDrive
、Memory
、CPU
这三个类分别代表电脑的硬盘、内存和 CPU 子系统,各自有对应的公开方法(readData
、loadData
、processData
)来执行相应功能。ComputerFacade
类作为外观类,聚合了HardDrive
、Memory
、CPU
这三个类的实例,并且有startComputer
方法用于按顺序调用子系统相关方法来模拟启动电脑的操作,同时有构造函数用于初始化这些子系统实例。Main
类是客户端类,在其main
方法中创建ComputerFacade
的实例并调用startComputer
方法来启动电脑,体现了客户端与外观类之间的使用关系。
流程图:
- 首先创建
ComputerFacade
实例,接着调用其startComputer
方法。 - 在
startComputer
方法内部,会依次执行硬盘读取数据、内存加载数据、CPU 处理数据的操作,最后输出电脑启动成功的提示信息,完成整个启动流程。
java
// 子系统1 - 硬盘
class HardDrive {
public void readData() {
System.out.println("Hard drive is reading data...");
}
}
// 子系统2 - 内存
class Memory {
public void loadData() {
System.out.println("Memory is loading data...");
}
}
// 子系统3 - CPU
class CPU {
public void processData() {
System.out.println("CPU is processing data...");
}
}
// 外观类
class ComputerFacade {
private HardDrive hardDrive;
private Memory memory;
private CPU cpu;
public ComputerFacade() {
hardDrive = new HardDrive();
memory = new Memory();
cpu = new CPU();
}
public void startComputer() {
hardDrive.readData();
memory.loadData();
cpu.processData();
System.out.println("Computer started successfully.");
}
}
// 客户端
public class Main {
public static void main(String[] args) {
ComputerFacade computerFacade = new ComputerFacade();
computerFacade.startComputer();
}
}
C++示例
cpp
#include <iostream>
// 子系统1 - 硬盘
class HardDrive {
public:
void readData() {
std::cout << "Hard drive is reading data..." << std::endl;
}
};
// 子系统2 - 内存
class Memory {
public:
void loadData() {
std::cout << "Memory is loading data..." << std::endl;
}
};
// 子系统3 - CPU
class CPU {
public:
void processData() {
std::cout << "CPU is processing data..." << std::endl;
}
};
// 外观类
class ComputerFacade {
private:
HardDrive hardDrive;
Memory memory;
CPU cpu;
public:
ComputerFacade() {}
void startComputer() {
hardDrive.readData();
memory.loadData();
cpu.processData();
std::cout << "Computer started successfully." << std::endl;
}
};
// 客户端
int main() {
ComputerFacade computerFacade;
computerFacade.startComputer();
return 0;
}
Python示例
python
# 子系统1 - 硬盘
class HardDrive:
def read_data(self):
print("Hard drive is reading data...")
# 子系统2 - 内存
class Memory:
def load_data(self):
print("Memory is loading data...")
# 子系统3 - CPU
class CPU:
def process_data(self):
print("CPU is processing data...")
# 外观类
class ComputerFacade:
def __init__(self):
self.hard_drive = HardDrive()
self.memory = Memory()
self.cpu = CPU()
def start_computer(self):
self.hard_drive.read_data()
self.memory.load_data()
self.cpu.process_data()
print("Computer started successfully.")
# 客户端
if __name__ == "__main__":
computer_facade = ComputerFacade()
computer_facade.start_computer()
Go示例
Go
package main
import "fmt"
// 子系统1 - 硬盘
type HardDrive struct{}
func (h *HardDrive) readData() {
fmt.Println("Hard drive is reading data...")
}
// 子系统2 - 内存
type Memory struct{}
func (m *Memory) loadData() {
fmt.Println("Memory is loading data...")
}
// 子系统3 - CPU
type CPU struct{}
func (c *CPU) processData() {
fmt.Println("CPU is processing data...")
}
// 外观类
type ComputerFacade struct {
hardDrive HardDrive
memory Memory
cpu CPU
}
func NewComputerFacade() *ComputerFacade {
return &ComputerFacade{
hardDrive: HardDrive{},
memory: Memory{},
cpu: CPU{},
}
}
func (c *ComputerFacade) startComputer() {
c.hardDrive.readData()
c.memory.loadData()
c.cpu.processData()
fmt.Println("Computer started successfully.")
}
func main() {
computerFacade := NewComputerFacade()
computerFacade.startComputer()
}
八、外观模式的优缺点
优点
- 简化客户端代码:客户端不需要了解子系统内部的复杂结构和操作流程,只需要调用外观类提供的简单接口,降低了客户端与子系统之间的耦合度。
- 提高可维护性:如果子系统内部发生变化,只要外观类的接口不变,客户端不需要修改代码。而且在外观类内部可以方便地对子系统的调用逻辑进行调整。
- 增强系统安全性:外观类可以控制对子系统的访问权限,只暴露必要的操作给客户端,隐藏子系统内部的敏感信息和操作。
缺点
- 外观类可能变得复杂:如果子系统过于复杂,外观类可能需要承担过多的职责,导致外观类本身变得臃肿和复杂,违背了单一职责原则。
- 不符合开闭原则:当需要在子系统中添加新的功能或者子系统时,如果这个新功能不能通过外观类现有的接口提供,可能需要修改外观类的代码,这在一定程度上违反了开闭原则。
九、外观模式的升级版
外观模式的升级版可以是与其他设计模式结合,例如与代理模式结合。在这种情况下,外观类不仅提供了子系统的统一接口,还可以在代理层增加额外的功能,如权限验证、性能监控等。另外,也可以考虑将外观模式应用在微服务架构中,将多个微服务的交互封装在一个外观服务中,这个外观服务可以提供更高级别的业务操作,并且可以在外观服务中进行服务治理(如熔断、限流等),以提高整个微服务系统的稳定性和可用性。