设计模式 - 建造者模式

本贴最后更新于 882 天前,其中的信息可能已经时移世改

建造者模式

image.png

我们通过一个案例来学习建造者模式。

生成简单form表单

通过代码生成表单是很多应用程序的主要任务。我们首先编写一个简单函数生成一个小的表单。

def generate_webform(field_list):
    generate_fields = "\n".join(map(
        lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x), field_list
    ))
    return "<form>{}</form>".format(generate_fields)
if __name__ == '__main__':
    fields = ["name", "age", "email", "telephone"]
    print(generate_webform(fields))

这段代码有局限性,只能生成文本字段。再进一步,写一个函数来生成html文件。

def generate_webform(field_list):
    generate_fields = "\n".join(map(
        lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x), field_list
    ))
    return "<form>{}</form>".format(generate_fields)


def build_html_form(fields):
    with open('form_file.html', 'w') as f:
        f.write(
            "<html><body>{}</body></html>".format(generate_webform(fields))
        )


if __name__ == '__main__':
    fields = ["name", "age", "email", "telephone"]
    build_html_form(fields)

这个文件可以在浏览器中打开。

image.png

生成不同类型的字段

通过列表参数

上面的代码只能生成文本字段,而表单可以具有许多其他类型的字段。可以通过参数实现生成不同类型的字段,看下面的代码,我们再表单中添加复选框。

def generate_webform(text_field_list=[], checkbox_field_list=[]):
    generate_fields = "\n".join(map(
        lambda x: '{0}:<br><input type="text" name="{0}"><br>'.format(x), text_field_list
    ))
    generate_fields += "\n".join(map(
        lambda x: '<label><input type="checkbox" id="{0}" value="{0}">{0}<br>'.format(x), checkbox_field_list
    ))
    return "<form>{}</form>".format(generate_fields)


def build_html_form(text_field_list=[], checkbox_field_list=[]):
    with open('form_file.html', 'w') as f:
        f.write(
            "<html><body>{}</body></html>".format(generate_webform(text_field_list, checkbox_field_list))
        )


if __name__ == '__main__':
    text_fields = ["name", "age", "email", "telephone"]
    checkbox_fields = ["awesome", "bad"]
    build_html_form(text_fields, checkbox_fields)

这个方法有一些明显的问题,首先就是无法处理具有默认值或者选项或者超出字段名称的任何信息的字段。

通过字典参数

现在我们扩展这个表单生成函数的功能,以便可以应对大量的字段类型,其中每个类型都有自己的设置。我们还面临一个问题,目前没有办法交替使用不同类型的字段。

接下来我们以字典数据来描述字段,然后以字典列表作为参数传入生成函数,代码如下:

def generate_webform(field_dict_list):
    generated_field_list = []
    for field in field_dict_list:
        if field['type'] == 'text':
            generated_field_list.append(
                '{0}:<br><input type="text" name="{1}"><br>'.format(field['label'], field['name'])
            )
        if field['type'] == 'password':
            generated_field_list.append(
                '{0}:<br><input type="password" name="{1}"><br>'.format(field['label'], field['name'])
            )
        elif field['type'] == 'checkbox':
            generated_field_list.append(
                '<label><input type="checkbox" id="{0}" value="{1}">{2}<br>'.format(
                    field['id'],
                    field['value'],
                    field['label']
                )
            )
    generated_fields = '\n'.join(generated_field_list)

    return "<form>{}</form>".format(generated_fields)


def build_html_form(field_list):
    with open('form_file.html', 'w', encoding='utf-8') as f:
        f.write(
            "<html><body>{}</body></html>".format(generate_webform(field_list))
        )


if __name__ == '__main__':
    field_list = [
        {
            'type': 'text',
            'label': 'username',
            'name': 'username'
        },
        {
            'type': 'password',
            'label': 'password',
            'name': 'password'
        },
        {
            'type': 'checkbox',
            'label': 'remember me',
            'id': 'remember',
            'value': '1'
        }
    ]
    build_html_form(field_list)

每一个字段字典包含一个type字段,它表示该字段的类型,然后还可以包含一些其他字段,例如标签,名称,值等。但是将循环和条件语句堆加到一起,当再添加一些字段时,代码会变得难以阅读和维护。

函数优化

我们将不同的字段html生成部分单独封装成不同的函数,代码如下:

def generate_webform(field_dict_list):
    generated_field_list = []
    for field in field_dict_list:
        if field['type'] == 'text':
            field_html = generate_text_field(field)
        if field['type'] == 'password':
            field_html = generate_password_field(field)
        elif field['type'] == 'checkbox':
            field_html = generate_checkbox_field(field)
        generated_field_list.append(field_html)
    generated_fields = '\n'.join(generated_field_list)

    return "<form>{}</form>".format(generated_fields)


def generate_text_field(field):
    return '{0}:<br><input type="text" name="{1}"><br>'.format(field['label'], field['name'])


def generate_password_field(field):
    return '{0}:<br><input type="password" name="{1}"><br>'.format(field['label'], field['name'])


def generate_checkbox_field(field):
    return '<label><input type="checkbox" id="{0}" value="{1}">{2}<br>'.format(
                    field['id'],
                    field['value'],
                    field['label']
                )


def build_html_form(field_list):
    with open('form_file.html', 'w', encoding='utf-8') as f:
        f.write(
            "<html><body>{}</body></html>".format(generate_webform(field_list))
        )


if __name__ == '__main__':
    field_list = [
        {
            'type': 'text',
            'label': 'username',
            'name': 'username'
        },
        {
            'type': 'password',
            'label': 'password',
            'name': 'password'
        },
        {
            'type': 'checkbox',
            'label': 'remember me',
            'id': 'remember',
            'value': '1'
        }
    ]
    build_html_form(field_list)

还是使用了if语句,不过至少代码变得清晰整洁了一些。但是当更多的字段类型添加到表单生成器时,这些if语句会更多。接下来我们尝试通过面向对象的方式改善这一情况。

面向对象

class HtmlField:
    def __init__(self, **kwargs):
        self.html = ''
        if kwargs['type'] == 'text':
            self.html = self.construct_text_field(kwargs['label'], kwargs['name'])
        elif kwargs['type'] == 'password':
            self.html = self.construct_password_field(kwargs['label'], kwargs['name'])
        elif kwargs['type'] == 'checkbox':
            self.html = self.construct_checkbox(kwargs['field_id'], kwargs['value'], kwargs['label'])

    def __str__(self):
        return self.html

    def construct_text_field(self, label, name):
        return '{0}:<br><input type="text" name="{1}"><br>'.format(label, name)

    def construct_password_field(self, label, name):
        return '{0}:<br><input type="password" name="{1}"><br>'.format(label, name)

    def construct_checkbox(self, field_id, value, label):
        return '<label><input type="checkbox" id="{0}" value="{1}">{2}<br>'.format(
            field_id,
            value,
            label
        )


def generate_webform(field_dict_list):
    generated_field_list = []
    for field in field_dict_list:
        try:
            generated_field_list.append(str(HtmlField(**field)))
        except Exception as e:
            print("error: {}".format(e))

    generated_fields = '\n'.join(generated_field_list)

    return "<form>{}</form>".format(generated_fields)


def build_html_form(field_list):
    with open('form_file.html', 'w', encoding='utf-8') as f:
        f.write(
            "<html><body>{}</body></html>".format(generate_webform(field_list))
        )


if __name__ == '__main__':
    field_list = [
        {
            'type': 'text',
            'label': 'username',
            'name': 'username'
        },
        {
            'type': 'password',
            'label': 'password',
            'name': 'password'
        },
        {
            'type': 'checkbox',
            'label': 'remember me',
            'field_id': 'remember',
            'value': '1'
        }
    ]
    build_html_form(field_list)

对于每一种字段,我们都需要一个构造函数。当需要构造多种形式的表单和字段时,会创建不同的字段类和构造方法,代码会变得难以维护。

建造者模式

在生活中一些比较复杂的商品都是由很多部件按一定的步骤组合而成的。

比如手机,由手机壳,屏幕,主板,等等组装而成。电脑由cpu,内存条,硬盘,显卡,主板,机箱,显示器等部件组装而成。在工厂中往往同一条生产线可以生产不同的产品,比如生产手机的生产线可以同时组装不同品牌的手机。

在软件开发的过程中,有时需要创建一些复杂的对象,这些对象向上面的产品一样也由很多子部件按照一定的步骤组合而成。

比如我们上面要生成的表单。我们把表单类比手机,表单由不同的字段构成,并且在不同设备上的表单的表形式也不同。

所以结合生活中的产品的生产和软件开发的流程总结了建造者模式。

它的定义是:将一个复杂对象的构造与它的表示分离,是同样的构造过程可以创建不同的表示,这样的设计模式被称为建造者模式。它将复杂对象分解为很多简单的对象,然后按照步骤进行组装。它将变与不变相分离,即产品的组成部分不变,但每一个部分可以灵活多变。

建造者模式的结构

建造者模式有两个主要的角色

  1. 建造者(builder):建造者是一个抽象类,它知道如何构建最终对象的所组成部分。在上面的例子中,它将指导如何构建每一个字段类型。并且可以将各个部分组装成一个完整的表单对象。
  2. 指挥者(director):director控制着构造过程。它会使用builder的一个(或多个)实例来构建表单。

python中的建造者模式通用实现看起来如下所示:

from abc import ABCMeta, abstractmethod


class Director(metaclass=ABCMeta):
    # 抽象指挥者
    def __init__(self):
        self._builder = None

    @abstractmethod
    def construct(self):
        # 抽象构造方法
        pass

    def get_constructed_object(self):
        return self._builder.constructed_object


class Builder(metaclass=ABCMeta):
    # 抽象建造者
    def __init__(self, constructed_object):
        self.constructed_object = constructed_object


class Product:
    # 产品
    def __init__(self):
        pass

    def __repr__(self):
        pass


class ConcreteBuilder(Builder):
    # 具体建造者
    pass


class ConcreteDirector(Director):
    # 具体指挥者

    pass


if __name__ == '__main__':
    ConcreteDirector()

我们看到了还有几个角色:

现在我们就使用建造模式,重新编写网页表单生成器。

案例

from abc import ABCMeta, abstractmethod


class Director(metaclass=ABCMeta):
    def __init__(self):
        self._builder = None

    def set_builder(self, builder):
        self._builder = builder

    @abstractmethod
    def construct(self, field_list):
        pass

    def get_constructed_object(self):
        return self._builder.constructed_object


class AbstractFormBuilder(metaclass=ABCMeta):
    def __init__(self):
        self.constructed_object = None

    @abstractmethod
    def add_text_field(self, field_dict):
        pass

    @abstractmethod
    def add_checkbox(self, checkbox_dict):
        pass

    @abstractmethod
    def add_password(self, password_dict):
        pass


class HtmlForm:
    def __init__(self):
        self.field_list = []

    def __repr__(self):
        return '<form>{}</form>'.format(''.join(self.field_list))


class HtmlFormBuilder(AbstractFormBuilder):
    def __init__(self):
        self.constructed_object = HtmlForm()

    def add_text_field(self, field_dict):
        self.constructed_object.field_list.append(
            '{0}:<br><input type="text" name="{1}"><br>'.format(field_dict['label'], field_dict['name'])
        )

    def add_checkbox(self, checkbox_dict):
        self.constructed_object.field_list.append(
            '<label><input type="checkbox" id="{0}" value="{1}">{2}<br>'.format(
                checkbox_dict['field_id'],
                checkbox_dict['value'],
                checkbox_dict['label']
            )
        )

    def add_password(self, password_dict):
        self.constructed_object.field_list.append(
            '{0}:<br><input type="password" name="{1}"><br>'.format(password_dict['label'], password_dict['name'])
        )


class FormDirector(Director):
    def __init__(self):
        super().__init__()

    def construct(self, field_list):
        for field in field_list:
            if field['type'] == 'text':
                self._builder.add_text_field(field)
            elif field['type'] == 'checkbox':
                self._builder.add_checkbox(field)
            elif field['type'] == 'password':
                self._builder.add_password(field)


if __name__ == '__main__':
    director = FormDirector()
    builder = HtmlFormBuilder()
    director.set_builder(builder)
    field_list = [
        {
            'type': 'text',
            'label': 'username',
            'name': 'username'
        },
        {
            'type': 'password',
            'label': 'password',
            'name': 'password'
        },
        {
            'type': 'checkbox',
            'label': 'remember me',
            'field_id': 'remember',
            'value': '1'
        }
    ]
    director.construct(field_list)
    with open('form_file.html', 'w') as f:
        f.write(

            "<html><body>{}</body></html>".format(director.get_constructed_object())

        )
    print(HtmlForm().field_list)

这段代码看起来比前面的代码更加的复杂。但是表单的创建逻辑被抽象出来了,我们现在可以用相同的构造过程来创建具有不同表现形式的表单。并且减小了对象的大小,能更好的控制构造过程,和修改对象的表现形式。缺点是,每一种表单类型都要创建具体的建造者。

应用场景

建造者模式主要适用于以下应用场景:

回帖
请输入回帖内容 ...