Python设计模式之外观模式

外观模式(facade)

系统会随着演化变得非常复杂,最终形成大量的(并且有时是令人迷惑的)类和交互,这种情况并不少见。许多情况下,我们并不想把这种复杂性暴露给客户端。外观设计模式有助于隐藏系统的内部复杂性,并通过一个简化的接口向客户端暴露必要的部分(请参考[Eckel08,第209 页])。本质上,外观(Facade)是在已有复杂系统之上实现的一个抽象层。

下图演示了外观的角色。这张图是Wikipedia上外观模式Java语言示例的类图表示(请参考网页[t.cn/Rqrl38m])。计算机是一个复杂的机器,全功能运行依赖多个部件。为简化表述,这里所说的计算机是指IBM衍生的那一类,使用冯·诺依曼架构。启动一台计算机是一个相当复杂的过程。CPU、内存以及硬盘都需要加电运行;引导加载程序需要从硬盘加载到内存,CPU则必须启动操作系统内核,等等。我们不会把这些复杂性暴露给客户端,而是创造一个外观来封装整个过程,并保证所有步骤按照正确的次序运行。

从图中展示的类可知,仅Computer类需要暴露给客户端代码。客户端仅执行Computer的Start()方法。所有其他复杂部件都由外观类Computer来维护。

# 请看如下所示,来自于github的例子:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

SLEEP = 0.1


# Complex Parts
class TC1:

    def run(self):
        print("###### In Test 1 ######")
        time.sleep(SLEEP)
        print("Setting up")
        time.sleep(SLEEP)
        print("Running test")
        time.sleep(SLEEP)
        print("Tearing down")
        time.sleep(SLEEP)
        print("Test Finished\n")


class TC2:

    def run(self):
        print("###### In Test 2 ######")
        time.sleep(SLEEP)
        print("Setting up")
        time.sleep(SLEEP)
        print("Running test")
        time.sleep(SLEEP)
        print("Tearing down")
        time.sleep(SLEEP)
        print("Test Finished\n")


class TC3:

    def run(self):
        print("###### In Test 3 ######")
        time.sleep(SLEEP)
        print("Setting up")
        time.sleep(SLEEP)
        print("Running test")
        time.sleep(SLEEP)
        print("Tearing down")
        time.sleep(SLEEP)
        print("Test Finished\n")


# Facade
class TestRunner:

    def __init__(self):
        self.tc1 = TC1()
        self.tc2 = TC2()
        self.tc3 = TC3()
        self.tests = [self.tc1, self.tc2, self.tc3]

    def runAll(self):
        [i.run() for i in self.tests]


# Client
if __name__ == '__main__':
    testrunner = TestRunner()
    testrunner.runAll()

### OUTPUT ###
# ###### In Test 1 ######
# Setting up
# Running test
# Tearing down
# Test Finished
#
# ###### In Test 2 ######
# Setting up
# Running test
# Tearing down
# Test Finished
#
# ###### In Test 3 ######
# Setting up
# Running test
# Tearing down
# Test Finished
#
###### In Test 1 ######
Setting up
Running test
Tearing down
Test Finished

###### In Test 2 ######
Setting up
Running test
Tearing down
Test Finished

###### In Test 3 ######
Setting up
Running test
Tearing down
Test Finished

现实生活中的例子

在现实中,外观模式相当常见。当你致电一个银行或公司,通常是先被连线到客服部门,客 服职员在你和业务部门(结算、技术支持、一般援助等)及帮你解决具体问题的职员之间充当一 个外观的角色。下图由sourcemaking.com提供,以图表形式展示了这个例子(请参考网页[t.cn/RqrlrtI])。

也可以将汽车或摩托车的启动钥匙视为一个外观。外观是激活一个系统的便捷方式,系统的 内部则非常复杂。当然,对于其他可以通过一个简单按钮就能激活的复杂电子设备,同样可以如 此看待,比如计算机。

软件中的例子

django-oscar-datacash模块是Django的一个第三方组件,用于集成DataCash支付网关。该组件有一个Gateway类,提供对多种DataCash API的细粒度访问。在那之上,它也包含一个Facade类,提供粗粒度API(提供给那些不需要处理细节的人),并针对审计目的提供保存事务的能力(请参考网页[t.cn/RqrlgCG])。

Caliendo是一个用于模拟Python API的的接口,它包含一个facade模块。该模块使用外观模式来完成许多不同但有用的事情(比如缓存方法),并基于传给顶层Facade方法的输入对象决定返回什么方法(请参考网页[t.cn/RqrlkiU])。

应用案例

使用外观模式的最常见理由是为一个复杂系统提供单个简单的入口点。引入外观之后,客户端代码通过简单地调用一个方法/函数就能使用一个系统。同时,内部系统并不会丢失任何功能,外观只是封装了内部系统。

不把系统的内部功能暴露给客户端代码有一个额外的好处:我们可以改变系统内部,但客户端代码不用关心这个改变,也不会受这个改变的影响。客户端代码不需要进行任何改变(请参考[Zlobin13,第44页])。

如果你的系统包含多层,外观模式也能派上用场。你可以为每一层引入一个外观入口点,并让所有层级通过它们的外观相互通信。这提高了层级之间的松耦合性,尽可能保持层级独立(请参考[GOF95,第209页])。

实现

假设我们想使用多服务进程方式实现一个操作系统,类似于MINIX 3(请参考网页 [t.cn/h5mI2X])或GNU Hurd(请参考网页[t.cn/RqrjZA1])那样。多服务进程的操作系统有一个极小的内核,称为微内核(microkernel),它在特权模式下运行。系统的所有其他服务都遵从一种服务架构(驱动程序服务器、进程服务器、文件服务器等)。每个服务进程属于一个不同的内存地址空间,以用户模式在微内核之上运行。这种方式的优势是操作系统更能容错、更加可靠、更加安全。例如,由于所有驱动程序都以用户模式在一个驱动服务进程之上运行,所以某个驱动程序中的一个bug并不能让整个系统崩溃,也无法影响到其他服务进程。其劣势则是性能开销和系统编程的复杂性,因为服务进程和微内核之间,还有独立的服务进程之间,使用消息传递方式进行通信。消息传递比宏内核(如Linux)所使用的共享内存模型更加复杂(请参考网页[t.cn/RqrjAK8])。

我们从Server接口(这里的“接口”并非指语法上的interface,而是指一个不能直接实例化的类)开始实现,使用一个Enum类型变量来描述一个服务进程的不同状态,使用abc模块来禁止对Server接口直接进行初始化,并强制子类实现关键的boot()和kill() 方法。这里假设每个服务进程的启动、关闭及重启都相应地需要不同的动作。如果你以前没用过abc模块,请记住以下几个重要事项。

  • 我们需要使用metaclass关键字来继承ABCMeta。
  • 使用@abstractmethod修饰器来声明Server的所有子类都应(强制性地)实现哪些方法。

尝试移除一个子类的boot()或kill()方法,看看会发生什么。移除@abstractmethod修饰器之后再试试。一切如你所料吗?

我们来思考以下这段代码。

from enum import Enum
from abc import ABCMeta, abstractmethod

State = Enum('State', 'new running sleeping restart zombie')

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)。除了Server接口要求实现的方法之外,每个服务进程还可以具有自己特有的方法。例如,FileServer有一个create_file()方法用于创建文件,ProcessServer有一个create_process()方法用于创建进程。

class FileServer(server):

    def __init__(self):
        '''初始化文件服务进程要求的操作'''
        self.name = 'FileServer'
        self.state = State.new

    def boot(self):
        print('booting the {}'.format(self))
        '''启动文件服务进程要求的操作'''
        self.state = State.running

    def kill(self, restart=True):
        print('Killing {}'.format(self))
        '''终止文件服务进程要求的操作'''
        self.state = State.restart if restart else State.zombie

    def create_file(self, user, name, permissions):
        '''检查访问权限的有效性和用户权限等'''
        print("trying to create the file '{}' for user '{}' with permissions {}".format(name, user, permissions))

class ProcessServer(server):
    def __init__(self):
        '''初始化进程服务进程要求的操作'''
        self.name = 'ProcessServer'
        self.state = State.new

    def boot(self):
        print('booting the {}'.format(self))
        '''启动进程服务进程要求的操作'''
        self.state = State.running

    def kill(self, restart=True):
        print('Killing {}'.format(self))
        '''终止进程服务进程要求的操作'''
        self.state = State.restart if restart else State.zombie

    def create_process(self, user, name):
        '''检查用户权限和生成PID等'''
        print("trying to create the process '{}' for user '{}'".format(name, user))

OperatingSystem类是一个外观。init()中创建所有需要的服务进程实例。start()方法是系统的入口点,供客户端代码使用。如果需要,可以添加更多的包装方法作为服务的访问点,比如包装方法create_file()和create_process()。从客户端的角度来看,所有服务都是由OperatingSystem类提供的。客户端并不应该被不必要的细节所干扰,比如,服务进程的存在和每个服务进程的责任。

class OperatingSystem:
    '''外观'''
    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)

在下面的完整代码清单中(文件facade.py),可以看到许多模拟的类和服务进程,它们的存在是为了让读者了解系统运转要求哪些抽象(User、Process和File等)和服务进程(WindowServer和NetworkServer等)。推荐至少实现系统的一个服务来练习一下(例如,文件创建)。可随意改变接口和方法签名来满足你的需求,但要确保在改变之后,客户端代码不需要知道OperatingSystem外观类之外的任何对象。

from enum import Enum
from abc import ABCMeta, abstractmethod

State = Enum('State', 'new running sleeping restart zombie')

class User:
    pass

class Process:
    pass

class File:
    pass

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

class FileServer(Server):
    def __init__(self):
        '''初始化文件服务进程要求的操作'''
        self.name = 'FileServer'
        self.state = State.new

    def boot(self):
        print('booting the {}'.format(self))
        '''启动文件服务进程要求的操作'''
        self.state = State.running

    def kill(self, restart=True):
        print('Killing {}'.format(self))
        '''终止文件服务进程要求的操作'''
        self.state = State.restart if restart else State.zombie

    def create_file(self, user, name, permissions):
        '''检查访问权限的有效性、用户权限等'''
        print("trying to create the file '{}' for user '{}' with permissions{}".format(name, user, permissions))

class ProcessServer(Server):
    def __init__(self):
        '''初始化进程服务进程要求的操作'''
        self.name = 'ProcessServer'
        self.state = State.new

    def boot(self):
        print('booting the {}'.format(self))
        '''启动进程服务进程要求的操作'''
        self.state = State.running

    def kill(self, restart=True):
        print('Killing {}'.format(self))
        '''终止进程服务进程要求的操作'''
        self.state = State.restart if restart else State.zombie

    def create_process(self, user, name):
        '''检查用户权限和生成PID等'''
        print("trying to create the process '{}' for user '{}'".format(name, user))

class WindowsServer:
    pass

class NetworkServer:
    pass

class OperatingSystem:
    '''外观'''
    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()
booting the FileServer
booting the ProcessServer
trying to create the file 'hello' for user 'foo' with permissions-rw-r-r
trying to create the process 'ls /tmp' for user 'bar'
如上所示:执行这个例子会显示两个存根服务进程的启动信息。

外观类OperatingSystem起到了很好的作用。客户端代码可以创建文件和进程,而无需知道操作系统的内部细节,比如,多个服务进程的存在。准确点说是客户端可以调用方法来创建文件和进程,但是目前它们是模拟的。如果感兴趣,你可以实现这两个方法之一作为练习,或者两个都实现。

小结

本章中,我们学习了如何使用外观模式。在客户端代码想要使用一个复杂系统但又不关心系统复杂性之时,这种模式是为复杂系统提供一个简单接口的理想方式。一台计算机是一个外观,因为当我们使用它时需要做的事情仅是按一个按钮来启动它;其余的所有硬件复杂性都用户无感知地交由BIOS、引导加载程序以及其他系统软件来处理。现实生活中外观的例子更多,比如,我们所致电的银行或公司客服部门,还有启动机动车所使用的钥匙。

我们讨论了两个使用外观的Django第三方组件:django-oscar-datacash和Caliendo。前者使用外观模式来提供一个简单的DataCash API以及保存事务的能力,后者为多种目的使用了外观,比如,缓存、基于输入对象的类型决定应该返回什么。

我们讲解了外观基本的应用案例,并以多服务进程操作系统使用的接口实现来结束本章内容。外观是一种隐藏系统复杂性的优雅方式,因为多数情况下客户端代码并不应该关心系统的这些细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容