設計模式 - 創(chuàng)建型模式(建造者模式)

簡介

想象一下,我們想要創(chuàng)建一個由多個部分構(gòu)成的對象,而且它的構(gòu)成需要一步接一步地完成。只有當各個部分都創(chuàng)建好,這個對象才算是完整的。這正是建造者設計模式(Builder design pattern)的用武之地。
建造者模式將一個復雜對象的構(gòu)造過程與其表現(xiàn)分離,這樣,同一個構(gòu)造 過程可用于創(chuàng)建多個不同的表現(xiàn)。

我們來看個實際的例子,這可能有助于理解建造者模式的目的。
假設我們想要創(chuàng)建一個 HTML頁面生成器,HTML頁面的基本結(jié)構(gòu)(構(gòu)造組件)通常是一樣的:以<html>開始</html> 結(jié)束,在HTML部分中有<head>和</head>元素,在head部分中又有<title>和</title>元素, 等等;但頁面在表現(xiàn)上可以不同。每個頁面有自己的頁面標題、文本標題以及不同的<body>內(nèi) 容。此外,頁面通常是經(jīng)過多個步驟創(chuàng)建完成的:有一個函數(shù)添加頁面標題,另一個添加主文本 標題,還有一個添加頁腳,等等。僅當一個頁面的結(jié)構(gòu)全部完成后,才能使用一個最終的渲染函 數(shù)將該頁面展示在客戶端。我們甚至可以更進一步擴展這個HTML生成器,讓它可以生成一些完 全不同的HTML頁面。一個頁面可能包含表格,另一個頁面可能包含圖像庫,還有一個頁面包含 聯(lián)系表單,等等。

HTML頁面生成問題可以使用建造者模式來解決。

建造者模式中,有兩個參與者:建造者(builder)指揮者(director)。
建造者負責創(chuàng)建復雜對象的各個組成部分。在HTML例子中,這些組成部 分是頁面標題、文本標題、內(nèi)容主體及頁腳。
指揮者使用一個建造者實例控制建造的過程。對于 HTML示例,這是指調(diào)用建造者的函數(shù)設置頁面標題、文本標題等。使用不同的建造者實例讓我 們可以創(chuàng)建不同的HTML頁面,而無需變更指揮者的代碼。

現(xiàn)實生活中的例子

快餐店使用的就是建造者設計模式。
即使存在多種漢堡包(經(jīng)典款、奶酪漢堡包等)和不同 包裝(小盒子、中等大小盒子等),準備一個漢堡包及打包(盒子或紙袋)的流程都是相同的。
經(jīng)典款漢堡包和奶酪漢堡包之間的區(qū)別在于表現(xiàn),而不是建造過程。指揮者是出納員,將需要準 備什么餐品的指令傳達給工作人員,建造者是工作人員中的個體,關注具體的順序。

下圖展示了統(tǒng)一建模語言(UML)的流程圖,說明當一個兒童套餐下單時,發(fā)生在顧客(客戶端)、出納員(指揮者)、工作人員(建造者)之間的信息交流。

圖片.png

軟件的例子

本章一開始提到的HTML例子,
1.在 django-widgy中得到了實際應用。django-widgy是一個 Django的第三方樹編輯器擴展,可用作內(nèi)容管理系統(tǒng)(Content Management System,CMS)。它 包含一個網(wǎng)頁構(gòu)建器,用來創(chuàng)建具有不同布局的HTML頁面。

2.django-query-builder是另一個基于建造者模式的Django第三方擴展庫,該擴展庫可用于動態(tài) 地構(gòu)建SQL查詢。使用它,我們能夠控制一個查詢的方方面面,并能創(chuàng)建不同種類的查詢,從簡 單的到非常復雜的都可以。

應用案例

如果我們知道一個對象必須經(jīng)過多個步驟來創(chuàng)建,并且要求同一個構(gòu)造過程可以產(chǎn)生不同的 表現(xiàn),就可以使用建造者模式。
這種需求存在于許多應用中,例如頁面生成器(本章提到的HTML 頁面生成器之類)、文檔轉(zhuǎn)換器以及用戶界面(User Interface, UI)表單創(chuàng)建工具。

有些資料提到建造者模式也可用于解決可伸縮構(gòu)造函數(shù)問題。當我們?yōu)橹С植煌膶ο髣?chuàng)建方式而不得不創(chuàng)建一個新的構(gòu)造函數(shù)時,可伸縮構(gòu)造函數(shù)問題就發(fā)生了,這種情況最終產(chǎn)生許多構(gòu)造函數(shù)和長長的形參列表,難以管理。Stack Overflow網(wǎng)站上列 出了一個可伸縮構(gòu)造函數(shù)的例子:https://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern/1953567#1953567。
幸運的是,這個問題在Python 中并不存在,因為至少有以下兩種方式可以解決這個問題。

  • 使用命名形參(請參考網(wǎng)頁[t.cn/RqBrUyV])
  • 使用實參列表展開(請參考網(wǎng)頁[t.cn/RyHhfg3)])

在這一點上,建造者模式和工廠模式的差別并不太明確。主要的區(qū)別在于工廠模式以單個步 驟創(chuàng)建對象,而建造者模式以多個步驟創(chuàng)建對象,并且?guī)缀跏冀K會使用一個指揮者。一些有針對 性的建造者模式實現(xiàn)并未使用指揮者,如Java的StringBuilder,但這只是例外。

另一個區(qū)別是,在工廠模式下,會立即返回一個創(chuàng)建好的對象;而在建造者模式下,僅在需 要時客戶端代碼才顯式地請求指揮者返回最終的對象。

新電腦類比的例子也許有助于區(qū)分建造者模式和工廠模式。假設你想購買一臺新電腦,如果 決定購買一臺特定的預配置的電腦型號,例如,最新的蘋果1.4GHz Mac mini,則是在使用工廠 模式。所有硬件的規(guī)格都已經(jīng)由制造商預先確定,制造商不用向你咨詢就知道自己該做些什么, 它們通常接收的僅僅是單條指令。在代碼級別上,看起來是下面這樣的(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類。這是禁止直接實例化一個類的簡潔方式。這里沒有表現(xiàn)出定制化。

另一個選擇是購買一臺定制的PC。假若這樣,使用的即是建造者模式。你是指揮者,向制 造商(建造者)提供指令說明心中理想的電腦規(guī)格。在代碼方面,看起來是下面這樣的
(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以及一步接一步裝配一臺電腦的過程,這樣現(xiàn)在就支持不同的配置了(注意,memory、hdd及gpu是 形參,并未預先設置)。想一下如果我們想要支持平板電腦的裝配,那又需要做些什么呢?

實現(xiàn)

讓我們來看看如何使用建造者設計模式實現(xiàn)一個比薩訂購的應用。比薩的例子特別有意思,因為準備好一個比薩需經(jīng)過多步操作,且這些操作要遵從特定順序。要添加調(diào)味料,你得先準備 生面團。要添加配料,你得先添加調(diào)味料。并且只有當生面團上放了調(diào)味料和配料之后才能開始 烤比薩。此外,每個比薩通常要求的烘培時間都不一樣,依賴于生面團的厚度和使用的配料。

先導入要求的模塊,聲明一些Enum參數(shù)以及一個在應用中會使用多次的常量。
常量STEP_DELAY用于在準備一個比薩的不同步驟(準備生面團、添加調(diào)味料等)之間添加時間延遲,如下所示。

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 # 考慮是示例,所以單位為秒

最終的產(chǎn)品是一個比薩,由Pizza類描述。若使用建造者模式,則最終產(chǎn)品(類)并沒有多 少職責,因為它不支持直接實例化。建造者會創(chuàng)建一個最終產(chǎn)品的實例,并確保這個實例完全準 備好。這就是Pizza類這么短小的緣由。
它只是將所有數(shù)據(jù)初始化為合理的默認值,唯一的例外 是方法prepare_dough()。將prepare_dough方法定義在Pizza類而不是建造者中,是考慮到 以下兩點。

1.為了澄清一點,就是雖然最終產(chǎn)品類通常會最小化,但這并不意味著絕不應該給它分配 任何職責。
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)。
每個建造者都創(chuàng)建一個Pizza實例,并包含遵從比薩制作 流程的方法:prepare_dough()、add_sauce、add_topping()和bake()。準確來說,其中 的prepare_dough只是對Pizza類中prepare_dough()方法的一層封裝。
注意每個建造者是如 何處理所有比薩相關細節(jié)的。例如,瑪格麗特比薩的配料是雙層馬蘇里拉奶酪(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 cre?me frai?che sauce to your creamy bacon')
            self.pizza.sauce = PizzaSauce.creme_fraiche
            time.sleep(STEP_DELAY)
            print('done with the cre?me frai?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方法,該方法接受一個建造者作為參數(shù),并以正確的順序執(zhí)行比薩的所有準備步驟。選擇恰當?shù)慕ㄔ煺?甚至可 以在運行時選擇),無需修改指揮者(Waiter)的任何代碼,就能制作不同的比薩。Waiter類 還包含pizza()方法,會向調(diào)用者返回最終產(chǎn)品(準備好的比薩),如下所示。

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

函數(shù)validate_style()類似于第1章中描述的validate_age()函數(shù),用于確保用戶提供 有效的輸入,當前案例中這個輸入是映射到一個比薩建造者的字符;輸入字符m表示使用 MargaritaBuilder類,輸入字符c則使用CreamyBaconBuilder類。這些映射關系存儲在參數(shù) builder中。該函數(shù)會返回一個元組,如果輸入有效,則元組的第一個元素被設置為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)  # 返回元組

# 實現(xiàn)的最后一部分是main()函數(shù)。main()函數(shù)實例化一個比薩建造者,然后指揮者Waiter
# 使用比薩建造者來準備比薩。創(chuàng)建好的比薩可在稍后的時間點交付給客戶端。

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)

小結(jié)

本章中,我們學習了如何使用建造者設計模式。
可以在工廠模式(工廠方法或抽象工廠)不 適用的一些場景中使用建造者模式創(chuàng)建對象。
在以下幾種情況下,與工廠模式相比,建造者模式 是更好的選擇。

  • 想要創(chuàng)建一個復雜對象(對象由多個部分構(gòu)成,且對象的創(chuàng)建要經(jīng)過多個不同的步驟, 這些步驟也許還需遵從特定的順序)
  • 要求一個對象能有不同的表現(xiàn),并希望將對象的構(gòu)造與表現(xiàn)解耦
  • 想要在某個時間點創(chuàng)建對象,但在稍后的時間點再訪問

我們看到了快餐店如何將建造者模式用于準備食物,兩個第三方Django擴展包(django-widgy 和django-query-builder)各自如何使用建造者模式來生成HTML頁面和動態(tài)的SQL查詢。我們重 點學習了建造者模式與工廠模式之間的區(qū)別,通過對預先配置(工廠)電腦與客戶定制(建造者) 電腦進行訂單類比來理清這兩種設計模式。

在實現(xiàn)部分,我們學習了如何創(chuàng)建一個比薩訂購應用,該應用能處理比薩準備過程的步驟依 賴。本章推薦了很多有趣的練習題,包括實現(xiàn)一個流利的建造者模式。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 建造者模式 想象一下,我們想要創(chuàng)建一個由多個部分構(gòu)成的對象,而且它的構(gòu)成需要一步接一步地完成。只有當各個部分都創(chuàng)建...
    英武閱讀 2,282評論 1 50
  • 版權: https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若與閱讀 23,558評論 3 240
  • 【學習難度:★★★★☆,使用頻率:★★☆☆☆】直接出處:建造者模式梳理和學習:https://github.com...
    BruceOuyang閱讀 864評論 0 5
  • NO.31 任何困境,都源于知識和方法的匱乏。因為匱乏所以稀缺,因而有價值。但當今知識和方法泛濫,兩者匱乏的背后應...
    思維工具箱閱讀 338評論 0 0
  • 有一些情侶,走著走著,就走不下去了,不知道該怎么繼續(xù)了。于是,有了分手。畢竟,一輩子的幸福,不能將就,不能湊合。走...
    小考拉俱樂部閱讀 174評論 2 0

友情鏈接更多精彩內(nèi)容