设计模式 - 创建型模式(建造者模式)

简介

想象一下,我们想要创建一个由多个部分构成的对象,而且它的构成需要一步接一步地完成。只有当各个部分都创建好,这个对象才算是完整的。这正是建造者设计模式(Builder design pattern)的用武之地。
建造者模式将一个复杂对象的构造过程与其表现分离,这样,同一个构造 过程可用于创建多个不同的表现。

我们来看个实际的例子,这可能有助于理解建造者模式的目的。
假设我们想要创建一个 HTML页面生成器,HTML页面的基本结构(构造组件)通常是一样的:以<html>开始</html> 结束,在HTML部分中有<head>和</head>元素,在head部分中又有<title>和</title>元素, 等等;但页面在表现上可以不同。每个页面有自己的页面标题、文本标题以及不同的<body>内 容。此外,页面通常是经过多个步骤创建完成的:有一个函数添加页面标题,另一个添加主文本 标题,还有一个添加页脚,等等。仅当一个页面的结构全部完成后,才能使用一个最终的渲染函 数将该页面展示在客户端。我们甚至可以更进一步扩展这个HTML生成器,让它可以生成一些完 全不同的HTML页面。一个页面可能包含表格,另一个页面可能包含图像库,还有一个页面包含 联系表单,等等。

HTML页面生成问题可以使用建造者模式来解决。

建造者模式中,有两个参与者:建造者(builder)指挥者(director)
建造者负责创建复杂对象的各个组成部分。在HTML例子中,这些组成部 分是页面标题、文本标题、内容主体及页脚。
指挥者使用一个建造者实例控制建造的过程。对于 HTML示例,这是指调用建造者的函数设置页面标题、文本标题等。使用不同的建造者实例让我 们可以创建不同的HTML页面,而无需变更指挥者的代码。

现实生活中的例子

快餐店使用的就是建造者设计模式。
即使存在多种汉堡包(经典款、奶酪汉堡包等)和不同 包装(小盒子、中等大小盒子等),准备一个汉堡包及打包(盒子或纸袋)的流程都是相同的
经典款汉堡包和奶酪汉堡包之间的区别在于表现,而不是建造过程。指挥者是出纳员,将需要准 备什么餐品的指令传达给工作人员,建造者是工作人员中的个体,关注具体的顺序。

下图展示了统一建模语言(UML)的流程图,说明当一个儿童套餐下单时,发生在顾客(客户端)、出纳员(指挥者)、工作人员(建造者)之间的信息交流。

图片.png

软件的例子

本章一开始提到的HTML例子,
1.在 django-widgy中得到了实际应用。django-widgy是一个 Django的第三方树编辑器扩展,可用作内容管理系统(Content Management System,CMS)。它 包含一个网页构建器,用来创建具有不同布局的HTML页面。

2.django-query-builder是另一个基于建造者模式的Django第三方扩展库,该扩展库可用于动态 地构建SQL查询。使用它,我们能够控制一个查询的方方面面,并能创建不同种类的查询,从简 单的到非常复杂的都可以。

应用案例

如果我们知道一个对象必须经过多个步骤来创建,并且要求同一个构造过程可以产生不同的 表现,就可以使用建造者模式。
这种需求存在于许多应用中,例如页面生成器(本章提到的HTML 页面生成器之类)、文档转换器以及用户界面(User Interface, UI)表单创建工具。

有些资料提到建造者模式也可用于解决可伸缩构造函数问题。当我们为支持不同的对象创建方式而不得不创建一个新的构造函数时,可伸缩构造函数问题就发生了,这种情况最终产生许多构造函数和长长的形参列表,难以管理。Stack Overflow网站上列 出了一个可伸缩构造函数的例子:https://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern/1953567#1953567
幸运的是,这个问题在Python 中并不存在,因为至少有以下两种方式可以解决这个问题。

  • 使用命名形参(请参考网页[t.cn/RqBrUyV])
  • 使用实参列表展开(请参考网页[t.cn/RyHhfg3)])

在这一点上,建造者模式和工厂模式的差别并不太明确。主要的区别在于工厂模式以单个步 骤创建对象,而建造者模式以多个步骤创建对象,并且几乎始终会使用一个指挥者。一些有针对 性的建造者模式实现并未使用指挥者,如Java的StringBuilder,但这只是例外。

另一个区别是,在工厂模式下,会立即返回一个创建好的对象;而在建造者模式下,仅在需 要时客户端代码才显式地请求指挥者返回最终的对象。

新电脑类比的例子也许有助于区分建造者模式和工厂模式。假设你想购买一台新电脑,如果 决定购买一台特定的预配置的电脑型号,例如,最新的苹果1.4GHz Mac mini,则是在使用工厂 模式。所有硬件的规格都已经由制造商预先确定,制造商不用向你咨询就知道自己该做些什么, 它们通常接收的仅仅是单条指令。在代码级别上,看起来是下面这样的(apple-factory.py)。

MINI14 = '1.4GHz Mac mini'

class AppleFactory:
    class MacMini14:
        def __init__(self):
            self.memory = 4
            self.hdd = 500 # 单位为GB
            self.gpu = 'Intel HD Graphics 5000'

        def __str__(self):
            info = ('Model: {}'.format(MINI14),
                         'Memory: {}GB'.format(self.memory),
                         'Hard Disk: {}GB'.format(self.hdd),
                         'Graphics Card: {}'.format(self.gpu))
            return '\n'.join(info)

    def build_computer(self, model):
        if model == MINI14:
            return self.MacMini14()
        else:
            print("I dont't know how to build {}".format(model))

if __name__ == '__main__':
    afac = AppleFactory()
    mac_mini = afac.build_computer(MINI14)
    print(mac_mini)

上面代码嵌套了MacMini14类。这是禁止直接实例化一个类的简洁方式。这里没有表现出定制化。

另一个选择是购买一台定制的PC。假若这样,使用的即是建造者模式。你是指挥者,向制 造商(建造者)提供指令说明心中理想的电脑规格。在代码方面,看起来是下面这样的
(computer-builder.py)。

class Computer:
    def __init__(self, serial_number):
        self.serial = serial_number
        self.memory = None # 单位为GB
        self.hdd = None
        self.gpu = None

    def __str__(self):
        info = ('Serial Number: {}'.format(self.serial),
                'Memory: {}GB'.format(self.memory),
                'Hard Disk: {}GB'.format(self.hdd),
                'Graphics Card: {}'.format(self.gpu))
        return '\n'.join(info)

class ComputerBuilder:
    # 这里是建造者
    def __init__(self):
        self.computer = Computer('AG23212121')  # 这里应该自动生成

    def configure_memory(self, amount):
        self.computer.memory = amount

    def configure_hdd(self, amount):
        self.computer.hdd = amount

    def configure_gpu(self, gpu_model):
        self.computer.gpu = gpu_model

class HardwareEngineer:
    # 硬件工程师是指挥者
    def __init__(self):
        self.builder = None

    def construct_computer(self, memory, hdd, gpu):
        self.builder = ComputerBuilder()

        self.builder.configure_memory(memory)
        self.builder.configure_hdd(hdd)
        self.builder.configure_gpu(gpu)

    @property
    def computer(self):
        return self.builder.computer

def main():
    engineer = HardwareEngineer()
    engineer.construct_computer(memory=8, hdd=500, gpu='GeForce GTX 650 Ti')
    computer = engineer.computer
    print(computer)

if __name__ == "__main__":
    main()

基本的变化是引入了一个建造者ComputerBuilder、一个指挥者HardwareEngineer以及一步接一步装配一台电脑的过程,这样现在就支持不同的配置了(注意,memory、hdd及gpu是 形参,并未预先设置)。想一下如果我们想要支持平板电脑的装配,那又需要做些什么呢?

实现

让我们来看看如何使用建造者设计模式实现一个比萨订购的应用。比萨的例子特别有意思,因为准备好一个比萨需经过多步操作,且这些操作要遵从特定顺序。要添加调味料,你得先准备 生面团。要添加配料,你得先添加调味料。并且只有当生面团上放了调味料和配料之后才能开始 烤比萨。此外,每个比萨通常要求的烘培时间都不一样,依赖于生面团的厚度和使用的配料。

先导入要求的模块,声明一些Enum参数以及一个在应用中会使用多次的常量。
常量STEP_DELAY用于在准备一个比萨的不同步骤(准备生面团、添加调味料等)之间添加时间延迟,如下所示。

from enum import Enum

PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready')
PizzaDough = Enum('PizzaDough', 'thin thick')
PizzaSauce = Enum('PizzaSauce', 'tomato creme_fraiche')
PizzaTopping = Enum('PizzaTopping', 'mozzarella double_mozzarella bacon ham mushrooms red_onion oregano')
STEP_DELAY = 3 # 考虑是示例,所以单位为秒

最终的产品是一个比萨,由Pizza类描述。若使用建造者模式,则最终产品(类)并没有多 少职责,因为它不支持直接实例化。建造者会创建一个最终产品的实例,并确保这个实例完全准 备好。这就是Pizza类这么短小的缘由。
它只是将所有数据初始化为合理的默认值,唯一的例外 是方法prepare_dough()。将prepare_dough方法定义在Pizza类而不是建造者中,是考虑到 以下两点。

1.为了澄清一点,就是虽然最终产品类通常会最小化,但这并不意味着绝不应该给它分配 任何职责。
2.为了通过组合提高代码复用。

class Pizza:
    def __init__(self, name):
        self.name = name
        self.dough = None
        self.sauce = None
        self.topping = []
    def __str__(self):
        return self.name
    def prepare_dough(self, dough):
        self.dough = dough
        print('preparing the {} dough of your {}...'.format(self.dough.name, self))   
        time.sleep(STEP_DELAY)
        print('done with the {} dough'.format(self.dough.name))

我们让该应用中有两个建造者:一个制作玛格丽特比萨(MargaritaBudiler),另一个制作奶油 熏肉比萨(CreamyBaconBuilder)。
每个建造者都创建一个Pizza实例,并包含遵从比萨制作 流程的方法:prepare_dough()、add_sauce、add_topping()和bake()。准确来说,其中 的prepare_dough只是对Pizza类中prepare_dough()方法的一层封装。
注意每个建造者是如 何处理所有比萨相关细节的。例如,玛格丽特比萨的配料是双层马苏里拉奶酪(mozzarella)和 牛至(oregano),而奶油熏肉比萨的配料是马苏里拉奶酪(mozzarella)、熏肉(bacon)、火腿(ham)、 蘑菇(mushrooms)、紫洋葱(red onion)和牛至(oregano),如下面的代码所示。

 class MargaritaBuilder:
        def __init__(self):
self.pizza = Pizza('margarita') self.progress = PizzaProgress.queued self.baking_time = 5 # 考虑是示例,单位为秒
        def prepare_dough(self):
            self.progress = PizzaProgress.preparation
            self.pizza.prepare_dough(PizzaDough.thin)
        def add_sauce(self):
            print('adding the tomato sauce to your margarita...')
            self.pizza.sauce = PizzaSauce.tomato
            time.sleep(STEP_DELAY)
            print('done with the tomato sauce')
            def add_topping(self): 13 print('adding the topping (double mozzarella, oregano) to your margarita') self.pizza.topping.append([i for i in (PizzaTopping.double_mozzarella, PizzaTopping.oregano)])
           time.sleep(STEP_DELAY)
           print('done with the topping (double mozzarella, oregano)')
     def bake(self):
         self.progress = PizzaProgress.baking
         print('baking your margarita for {}
         seconds'.format(self.baking_time))
         time.sleep(self.baking_time)
         self.progress = PizzaProgress.ready
         print('your margarita is ready')

class CreamyBaconBuilder:
        def __init__(self):
self.pizza = Pizza('creamy bacon') self.progress = PizzaProgress.queued self.baking_time = 7 # 考虑是示例,单位为秒
        def prepare_dough(self):
            self.progress = PizzaProgress.preparation
            self.pizza.prepare_dough(PizzaDough.thick)
        def add_sauce(self):
            print('adding the crème fraîche sauce to your creamy bacon')
            self.pizza.sauce = PizzaSauce.creme_fraiche
            time.sleep(STEP_DELAY)
            print('done with the crème fraîche sauce')
    def add_topping(self):
        print('adding the topping (mozzarella, bacon, ham,
mushrooms, red onion, oregano) to your creamy bacon') self.pizza.topping.append([t for t in (PizzaTopping.mozzarella, PizzaTopping.bacon,
        PizzaTopping.ham,PizzaTopping.mushrooms,
        PizzaTopping.red_onion, PizzaTopping.oregano)])
        time.sleep(STEP_DELAY)
        print('done with the topping (mozzarella, bacon, ham, mushrooms, red onion, oregano)')
    def bake(self):
        self.progress = PizzaProgress.baking
        print('baking your creamy bacon for {} seconds'.format(self.baking_time)) time.sleep(self.baking_time)
        self.progress = PizzaProgress.ready
        print('your creamy bacon is ready')

在这个例子中,指挥者就是服务员。Waiter类的核心是construct_pizza方法,该方法接受一个建造者作为参数,并以正确的顺序执行比萨的所有准备步骤。选择恰当的建造者(甚至可 以在运行时选择),无需修改指挥者(Waiter)的任何代码,就能制作不同的比萨。Waiter类 还包含pizza()方法,会向调用者返回最终产品(准备好的比萨),如下所示。

class Waiter:
    def __init__(self):
        self.builder = None

    def construct_pizza(self, builder):
        self.builder = builder
        [step() for step in (builder.prepare_dough, builder.add_sauce, builder.add_topping, builder.bake)]

    @property
    def pizza(self):
        return self.builder.pizza

函数validate_style()类似于第1章中描述的validate_age()函数,用于确保用户提供 有效的输入,当前案例中这个输入是映射到一个比萨建造者的字符;输入字符m表示使用 MargaritaBuilder类,输入字符c则使用CreamyBaconBuilder类。这些映射关系存储在参数 builder中。该函数会返回一个元组,如果输入有效,则元组的第一个元素被设置为True, 否 3 则为False,如下所示。

def validate_style(builders):
    try:
        pizza_style = input('What pizza would you like, [m]argarita or [c]reamy bacon?')
        builder = builders[pizza_style]()
        valid_input = True
    except KeyError as e:
        print('Sorry, only margarita (key m) and creamy bacon (key c) are available')
        return (False, None)

    return (True, builder)  # 返回元组

# 实现的最后一部分是main()函数。main()函数实例化一个比萨建造者,然后指挥者Waiter
# 使用比萨建造者来准备比萨。创建好的比萨可在稍后的时间点交付给客户端。

def main():
    builders = dict(m=MargaritaBuilder, c=CreamyBaconBuilder)
    valid_input = False
    while not valid_input:
        valid_input, builder = validate_style(builders)
    print()
    waiter = Waiter()
    waiter.construct_pizza(builder)
    pizza = waiter.pizza
    print()
    print('Enjoy your {}!'.format(pizza))

流利的建造者

class Pizza:
    def __init__(self, builder):
        self.garlic = builder.garlic
        self.extra_cheese  = builder.extra_cheese
    def __str__(self):
        garlic = 'yes' if self.garlic else 'no'
        cheese = 'yes' if self.extra_cheese else 'no'
        info = ('Garlic: {}'.format(garlic), 'Extra cheese: {}'.format(cheese)) return '\n'.join(info)
    class PizzaBuilder:
        def __init__(self):
            self.extra_cheese = False
            self.garlic = False

       def add_garlic(self):
            self.garlic = True
            return self
        def add_extra_cheese(self):
            self.extra_cheese = True
            return self
        def build(self):
            return Pizza(self)

if __name__ == '__main__':
    pizza = Pizza.PizzaBuilder().add_garlic().add_extra_cheese().build()
    print(pizza)

小结

本章中,我们学习了如何使用建造者设计模式。
可以在工厂模式(工厂方法或抽象工厂)不 适用的一些场景中使用建造者模式创建对象。
在以下几种情况下,与工厂模式相比,建造者模式 是更好的选择。

  • 想要创建一个复杂对象(对象由多个部分构成,且对象的创建要经过多个不同的步骤, 这些步骤也许还需遵从特定的顺序)
  • 要求一个对象能有不同的表现,并希望将对象的构造与表现解耦
  • 想要在某个时间点创建对象,但在稍后的时间点再访问

我们看到了快餐店如何将建造者模式用于准备食物,两个第三方Django扩展包(django-widgy 和django-query-builder)各自如何使用建造者模式来生成HTML页面和动态的SQL查询。我们重 点学习了建造者模式与工厂模式之间的区别,通过对预先配置(工厂)电脑与客户定制(建造者) 电脑进行订单类比来理清这两种设计模式。

在实现部分,我们学习了如何创建一个比萨订购应用,该应用能处理比萨准备过程的步骤依 赖。本章推荐了很多有趣的练习题,包括实现一个流利的建造者模式。

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

推荐阅读更多精彩内容

  • 建造者模式 想象一下,我们想要创建一个由多个部分构成的对象,而且它的构成需要一步接一步地完成。只有当各个部分都创建...
    英武阅读 2,157评论 1 50
  • 版权: https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若与阅读 22,841评论 3 242
  • 【学习难度:★★★★☆,使用频率:★★☆☆☆】直接出处:建造者模式梳理和学习:https://github.com...
    BruceOuyang阅读 734评论 0 5
  • NO.31 任何困境,都源于知识和方法的匮乏。因为匮乏所以稀缺,因而有价值。但当今知识和方法泛滥,两者匮乏的背后应...
    思维工具箱阅读 279评论 0 0
  • 有一些情侣,走着走着,就走不下去了,不知道该怎么继续了。于是,有了分手。毕竟,一辈子的幸福,不能将就,不能凑合。走...
    小考拉俱乐部阅读 105评论 2 0