外观模式(Facade Pattern)又叫门面模式。
随着系统的发展,它们会变得非常复杂,最终会有一个非常大甚至混乱的类和交互的集合,这是很正常的。在许多情况下,我们不希望将这种复杂性暴露给客户。
façade设计模式帮助我们隐藏系统的内部复杂性,并通过简化的接口只向客户端暴露出必要的东西。从本质上讲,façade是在现有的复杂系统上实现的抽象层。
让我们以计算机为例来说明问题。计算机是复杂的机器,依靠几个部分才能完全发挥作用。为了简单起见,"计算机 "这个词在这里指的是使用冯-诺依曼架构的IBM衍生产品。启动计算机是特别复杂的过程。CPU、主内存和硬盘需要启动和运行,启动加载器必须从硬盘加载到主内存,CPU必须启动操作系统内核,等等。我们没有把所有这些复杂的东西暴露给客户,而是创建了封装了整个程序的外观,确保所有的步骤都按照正确的顺序执行。
在对象设计和编程方面,我们应该有几个类,但只有计算机类需要暴露给客户端代码。例如,客户端只需要执行计算机类的start()方法,而其他所有复杂的部分都由计算机类来处理。
真实世界的例子
当你给银行或公司打电话时,你通常会先被连接到客户服务部门。客户服务部的员工在你和实际的部门(账单、技术支持、一般援助等)之间充当门面,而这个员工将帮助你解决你的具体问题。
开启汽车或摩托车的钥匙也可以被认为是一个门面。它是激活内部非常复杂的系统的简单方法。当然,其他复杂的电子设备也是如此,我们可以用一个按钮来激活它们,比如电脑。
在软件方面,django-oscar-datacash模块是一个Django第三方模块,与DataCash支付网关集成。该模块有一个网关类,提供对各种DataCash API的细粒度访问。在此基础上,它还提供了一个façade类,提供了粒度较小的API(对于那些不想弄乱细节的人来说),并且能够为审计目的保存交易。
应用
不把系统的内部功能暴露给客户代码,给我们带来了额外的好处:我们可以对系统进行修改,但客户代码仍然不知道这些修改,也不会受到影响。不需要对客户端代码进行修改。
如果你的系统中有多层,Façade也很有用。你可以为每层引入Façade入口点,并让所有的层通过它们的Façade相互通信。这促进了松耦合,并使各层尽可能保持独立。
实现
多服务器操作系统有一个最小的内核,称为微内核,它以特权模式运行。系统的所有其他服务都遵循服务器架构(驱动服务器、进程服务器、文件服务器,等等)。每个服务器都属于不同的内存地址空间,并在用户模式下运行于微内核之上。这种方法的优点是,操作系统可以变得更加容错、可靠和安全。例如,由于所有的驱动程序都在驱动服务器上以用户模式运行,一个驱动程序的错误不能使整个系统崩溃,也不能影响其他的服务器。这种方法的缺点是性能开销和系统编程的复杂性,因为服务器和微内核之间以及独立服务器之间的通信是通过消息传递进行的。消息传递比Linux等单片机内核中使用的共享内存模型更加复杂。
我们从一个服务器接口开始。一个Enum参数描述了一个服务器的不同可能状态。我们使用ABC模块禁止直接实例化服务器接口,并使基本的boot()和kill()方法成为强制性的,假设在启动、杀死和重启每个服务器时需要采取不同的行动。
class Server(metaclass=ABCMeta):
@abstractmethod
def __init__(self):
pass
def __str__(self):
return self.name
@abstractmethod
def boot(self):
pass
@abstractmethod
def kill(self, restart=True):
pass
模块化的操作系统可以有服务器:文件服务器、进程服务器、认证服务器、网络服务器、图形/窗口服务器,等等。下面的例子包括两个存根服务器:FileServer和ProcessServer。除了需要由服务器接口实现的方法外,每个服务器都可以有自己的特定方法。例如,FileServer有create_file()方法用于创建文件,而ProcessServer有create_process()方法用于创建进程。
class FileServer(Server):
def __init__(self):
'''actions required for initializing the file server'''
self.name = 'FileServer'
self.state = State.new
def boot(self):
print(f'booting the {self}')
'''actions required for booting the file server'''
self.state = State.running
def kill(self, restart=True):
print(f'Killing {self}')
'''actions required for killing the file server'''
self.state = State.restart if restart else State.zombie
def create_file(self, user, name, permissions):
'''check validity of permissions, user rights, etc.'''
print(f"trying to create the file '{name}' for user '{user}' with permissions {permissions}")
class ProcessServer(Server):
def __init__(self):
'''actions required for initializing the process server'''
self.name = 'ProcessServer'
self.state = State.new
def boot(self):
print(f'booting the {self}')
'''actions required for booting the process server'''
self.state = State.running
def kill(self, restart=True):
print(f'Killing {self}')
'''actions required for killing the process server'''
self.state = State.restart if restart else State.zombie
def create_process(self, user, name):
'''check user rights, generate PID, etc.'''
print(f"trying to create the process '{name}' for user '{user}'")
OperatingSystem 类是外观。在其 init() 中,所有必要的服务器实例都被创建。客户端代码使用的start()方法,是系统的入口。如果有必要,可以添加更多的封装方法,作为服务器服务的访问点,如封装器,create_file()和create_process()。从客户的角度来看,所有这些服务都是由OperatingSystem类提供的。客户端不应该被一些不必要的细节所迷惑,如服务器的存在和每个服务器的责任。
OperatingSystem类的代码如下。
class OperatingSystem:
'''The Facade'''
def __init__(self):
self.fs = FileServer()
self.ps = ProcessServer()
def start(self):
[i.boot() for i in (self.fs, self.ps)]
def create_file(self, user, name, permissions):
return self.fs.create_file(user, name, permissions)
def create_process(self, user, name):
return self.ps.create_process(user, name)
def main():
os = OperatingSystem()
os.start()
os.create_file('foo', 'hello', '-rw-r-r')
os.create_process('bar', 'ls /tmp')
if __name__ == '__main__':
main()
小结
在本章中,我们已经学会了如何使用façade模式。这种模式是为客户代码提供简单接口的理想选择,这些客户代码想要使用一个复杂的系统,但不需要意识到系统的复杂性。一台计算机有一个外观,因为我们使用它所需要做的就是按下按钮来打开它。所有其余的硬件复杂性都由BIOS、启动加载器和系统软件的其他组件透明地处理。还有更多现实生活中的例子,比如当我们连接到银行或公司的客户服务部门,以及我们用来开启车辆的钥匙。
我们讨论了一个使用Façade的Django第三方模块:django-oscar-datacash。它使用façade模式来提供一个简单的DataCash API和保存交易的能力。
我们介绍了façade的基本用例,并以多服务器操作系统使用的接口的实现来结束本章。façade是一种隐藏系统复杂性的优雅方式,因为在大多数情况下,客户端代码不应该知道这些细节。