《精通python设计模式》读书笔记之——创建型设计模式

前言:

这几天拜读了“图灵程序设计丛书”的《精通python设计模式》,个人感觉是一本不错的介绍python设计模式的专业书籍,本书介绍了16种设计模式,每种设计模式从基本简介、现实生活例子、软件的例子、应用案例、代码实现五个方面一一介绍,上手简单,学习条例清晰。推荐阅读!!!接下来就将我的读书笔记整理在这。

一、设计模式简介:

设计模式重要的部分可能就是它的名称。给模式起名的好处是大家相互交流时有共同的词汇。因此,如果你提交一些代码进行评审,同行评审者的反馈中提到“我认为这个地方你可以使用一个策略模式来代替……”,即使你不知道或不记得策略模式是什么,也可以立即去查阅。
随着编程语言的演进,一些设计模式(如单例)也随之过时,甚至成了反模式,另一些则被内置在编程语言中(如迭代器模式)。另外,也有一些新的模式诞生(比如Borg/Monostate)

关于设计模式有一些误解。第一个误解是,一开始写代码就应该使用设计模式。第二个误解是设计模式应随处使用。这会导致方案很复杂,夹杂着多余的接口和分层,而其实往往一个更简单直接的方案就足够了。设计模式并不是万能的,仅当代码确实存在坏味道、难以扩展维护时,才有使用的必要。

设计模式是被发现,而不是被发明出来的

二、设计模式三大基本类型

设计模式分三大部分:

  1. 第一部分介绍处理对象创建的设计模式,包括工厂模式、建造者模式、原型模式;
  2. 第二部分介绍处理一个系统中不同实体(类、对象等)之间关系的设计模式, 包括外观模式、享元模式等 ;
  3. 第三部分介绍处理系统实体之间通信的设计模式,包括责任链模式、观察者模式等。

三、创建型设计模式

处理对象创建相关的问题,目标是当直接创建对象(在Python中是通过_init_()函数实现的,)不太方便时,提供更好的方式。

①. 工厂模式

简介:
在工厂设计模式中,客户端可以请求一个对象,而无需知道这个对象来自哪里;也就是,使用哪个类来生成这个对象。工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直 接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象。通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度。

工厂通常有两种形式:
第一种是工厂方法(Factory Method),它是一个方法(或以地道的Python 术语来说,是一个函数),对不同的输入参数返回不同的对象;

现实生活的例子:
现实中用到工厂方法模式思想的一个例子是塑料玩具制造。制造塑料玩具的压塑粉都是一样的,但使用不同的塑料模具就能产出不同的外形。比如,有一个工厂方法,输入是目标外形(鸭 子或小车)的名称,输出则是要求的塑料外形。

软件的例子:
Django框架使用工厂方法模式来创建表单字段。Django的forms模块支持不同种类字段(CharField、EmailField)的创建和定制(max_length、required)

应用案例:
如果因为应用创建对象的代码分布在多个不同的地方,而不是仅在一个函数方法中,你发现没法跟踪这些对象,那么应该考虑使用工厂方法模式。工厂方法集中地在一个地方创建对象,使对象跟踪变得更容易。注意,创建多个工厂方法也完全没有问 题,实践中通常也这么做,对相似的对象创建进行逻辑分组,每个工厂方法负责一个分组。例如,有一个工厂方法负责连接到不同的数据库(MySQL、SQLite),另一个工厂方法负责创建要求的几何对象(圆形、三角形)等,若需要将对象的创建和使用解耦,工厂方法也能派上用场。
.
另外一个值得一提的应用案例与应用性能及内存使用相关。工厂方法可以在必要时创建新的对象,从而提高性能和内存使用率。若直接实例化类来创建对象,那么每次创建新对象就需要分配额外的内存(除非这个类内部使用了缓存,一般情况下不会这 样)。例如类的两次实例化的内存地址不同

第二种是抽象工厂
第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法
抽象工厂设计模式是抽象方法的一种泛化。概括来说,一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象

现实生活的例子:
汽车制造业应用了抽象工厂的思想。冲压不同汽车模型的部件(车门、仪表盘、车篷、挡泥板及反光镜等)所使用的机件是相同的。机件装配起来的模型随时可配置,且易于改变。

软件的例子:
程序包django_factory是一个用于在测试中创建Django模型的抽象工厂实现,可用来为支持测试专有属性的模型创建实例。这能让测试代码的可读性更高,且能避免共享不必要的代码,故有其存在的价值

应用案例:
为抽象工厂模式是工厂方法模式的一种泛化,所以它能提供相同的好处。这样会产生一个问题:我们怎么知道何时该使用工厂方法,何时又该使用抽象工厂?答案是, 通常一开始时使用工厂方法,因为它更简单。如果后来发现应用需要许多工厂方法,那么将创建 一系列对象的过程合并在一起更合理,从而最终引入抽象工厂。

工厂方法代码实现:

import xml.etree.ElementTree as etree
import json

class JSONConnector:
    """
    类JSONConnector解析JSON文件,通过parsed_data()方法以一个字典(dict)的形式 返回数据。
    修饰器property使parsed_data()显得更像一个常规的变量,而不是一个方法
    """
    def __init__(self, filepath):
        print(444)
        self.data = None
        with open(filepath, mode='r', encoding='utf-8') as f:
            self.data = json.load(f)

    @property
    def parsed_data(self):
        return self.data

class XMLConnector:
    """
    类XMLConnector解析XML文件,通过parsed_data()方法以xml.etree.Element列表的形式返回所有数据
    """
    def __init__(self, filepath):
        self.tree = etree.parse(filepath)

    @property
    def parsed_data(self):
        return self.tree

def connection_factory(filepath):
    """
    函数connection_factory是一个工厂方法,基于输入文件路径的扩展名返回一个 JSONConnector或XMLConnector的实例
    :param filepath:
    :return: 返回一个 JSONConnector或XMLConnector的实例
    """
    print(filepath)
    if filepath.endswith("json"):
        connector = JSONConnector
    elif filepath.endswith("xml"):
        connector = XMLConnector
    else:
        raise ValueError('Cannot connect to {}'.format(filepath))
    return connector(filepath)

def connect_to(filepath):
    """
    函数connect_to()对connection_factory()进行包装,添加了异常处理
    :param filepath:
    :return:返回一个JSONConnector或XMLConnector的实例对象
    """
    factory = None
    try:
        factory = connection_factory(filepath)
    except ValueError as ve:
        print(ve)
    return factory

def main(filepath):
    # 函数main()演示如何使用工厂方法设计模式。第一部分是确认异常处理是否有效
    data_factory = connect_to(filepath)
    # 第二部分是得到数据格式对应的处理
    new_data = data_factory.parsed_data
    # 第三部分是根据不同数据格式做特定的处理方法
    if type(data_factory) == XMLConnector:
        liars = new_data.findall(".//{}[{}='{}']".format('person', 'lastName', 'Liar'))
        print('found: {} persons'.format(len(liars)))
        for liar in liars:
            print('first name: {}'.format(liar.find('firstName').text))
            print('last name: {}'.format(liar.find('lastName').text))
            [print('phone number ({})'.format(p.attrib['type']), p.text)
             for p in liar.find('phoneNumbers')]
    if type(data_factory) == JSONConnector:
        for donut in new_data:
            print('name: {}'.format(donut['name']))
            print('price: ${}'.format(donut['ppu']))
            [print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]

if __name__ == '__main__':
    main("person.xml")

抽象工厂代码实现:

"""
我们正在创造一个游戏,或者想在应用中包含一个迷你游戏让用户娱乐娱乐。我们希望至少包含两个游戏,
一个面向孩子,一个面向成人。在运行时,基于用户输入,决定该创建哪个游戏并运行。游戏的创建部分由一个抽象工厂维护
"""
class Frog:
    """
    孩子的游戏,我们将该游戏命名为FrogWorld。主人公是一只青蛙,喜欢吃虫子。每个英雄都需要一个好名字,在这个例子中,
    这个名字在运行时由用户给定。方法interact_with()用于描述青蛙与障碍物(比如,虫子、迷宫或其他青蛙)之间的交互
    """
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))

class Bug:
    """
    障碍物可以有多种,但对于我们的例子,可以仅仅是虫子。当青蛙遇到一只虫子,只支持一 种动作,那就是吃掉它!
    """
    def __str__(self):
        return 'a bug'

    def action(self):
        return 'eats it'

class FrogWorld:
    """
    类FrogWorld是一个抽象工厂,其主要职责是创建游戏的主人公和障碍物。区分创建方法并使其名字通用(比如,make_character()
    和make_obstacle()),这让我们可以动态改变当前激活的工厂(也因此改变了当前激活的游戏),而无需进行任何代码变更。
    在一门静态语言中, 抽象工厂是一个抽象类/接口,具备一些空方法,但在Python中无需如此,因为类型是在运行时检测的
    """
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t------ Frog World -------'

    def make_character(self):
        return Frog(self.player_name)

    def make_obstacle(self):
        return Bug()

class Wizard:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def interact_with(self, obstacle):
        print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))

class Ork:
    def __str__(self):
        return 'an evil ork'

    def action(self):
        return 'kills it'

class WizardWorld:
    """
    WizardWorld游戏也类似。在故事中唯一的区别是男巫战怪兽(如兽人)而不是吃虫子!
    """
    def __init__(self, name):
        print(self)
        self.player_name = name

    def __str__(self):
        return '\n\n\t------ Wizard World -------'

    def make_character(self):
        return Wizard(self.player_name)

    def make_obstacle(self):
        return Ork()

class GameEnvironment:
    """
    类GameEnvironment是我们游戏的主入口。它接受factory作为输入,
    用其创建游戏的世界。方法play()则会启动hero和obstacle之间的交互
    """
    def __init__(self, factory):
        self.hero = factory.make_character()
        self.obstacle = factory.make_obstacle()

    def play(self):
        self.hero.interact_with(self.obstacle)

def validate_age(name):
    """
    函数validate_age()提示用户提供一个有效的年龄。如果年龄无效,则会返回一个元组, 其第一个元素设置为False。
    如果年龄没问题,元素的第一个元素则设置为True,但我们真正关 心的是元素的第二个元素,也就是用户提供的年龄
    :param name:
    :return: ()
    """
    try:
        age = input('Welcome {}. How old are you? '.format(name))
        age = int(age)
    except ValueError as err:
        print("Age {} is invalid, please try again...".format(age))
        return (False, age)
    return (True, age)

def main():
    """
    该函数请求用户的姓名和年龄,并根据用户的年龄决定该玩哪个游戏
    :return:
    """
    name = input("Hello. What's your name? ")
    valid_input = False
    while not valid_input:
        valid_input, age = validate_age(name)
    game = FrogWorld if age < 18 else WizardWorld
    environment = GameEnvironment(game(name))
    environment.play()

if __name__ == '__main__':
    main()

工厂模式总结:
两种模式都可以用于以下几种场景:
(a)想要追踪对象的创建时,
(b)想要将对象的创建与使用解耦时,
(c)想要优化应用的性能 和资源占用时。
.
工厂方法设计模式的实现是一个不属于任何类的单一函数,负责单一种类对象(一个形状、 一个连接点或者其他对象)的创建。
.
抽象工厂设计模式的实现是同属于单个类的许多个工厂方法用于创建一系列种类的相关对象(一辆车的部件、一个游戏的环境,或者其他对象)。

②.建造者模式

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

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

现实生活的例子:
快餐店使用的就是建造者设计模式。

软件的例子:
django-widgy是一个 Django的第三方树编辑器扩展,可用作内容管理系统(Content Management System,CMS)。它 包含一个网页构建器,用来创建具有不同布局的HTML页面
.
django-query-builder是另一个基于建造者模式的Django第三方扩展库,该扩展库可用于动态地构建SQL查询。使用它,我们能够控制一个查询的方方面面,并能创建不同种类的查询,从简 单的到非常复杂的都可以

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

代码实现

工厂模式买电脑
MINI14 = '1.4GHz Mac mini'

class AppleFactory:
    """
    工厂模式买电脑
    """
    class MacMini14:
        def __init__(self):
            self.memory = 4  # 单位为GB
            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)
建造者模式定制电脑

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

    def __str__(self):
        info = ('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('AG23385193')

    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()
        [
            i for i in (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(hdd=500, memory=8, gpu='GeForce GTX 650 Ti')
    computer = engineer.computer
    print(computer)

if __name__ == '__main__':
    main()

工厂模式和建造者模式的的区别:
在工厂模式下,会立即返回一个创建好的对象;而在建造者模式下,仅在需要时客户端代码才显式地请求指挥者返回最终的对象。
.
简单明了的例子:
买电脑的例子也许有助于区分建造者模式和工厂模式。
假设你想购买一台新电脑,如果决定购买一台特定的预配置的电脑型号,则是在使用工厂模式。
假设你想定制一台新电脑,使用的即是建造者模式。你是指挥者,向制造商(建造者)提供指令说明心中理想的电脑规格。

建造者模式的总结:
我们学习了如何使用建造者设计模式。可以在工厂模式(工厂方法或抽象工厂)不适用的一些场景中使用建造者模式创建对象。在以下几种情况下,与工厂模式相比,建造者模式是更好的选择。
1. 想要创建一个复杂对象(对象由多个部分构成,且对象的创建要经过多个不同的步骤, 这些步骤也许还需遵从特定的顺序)。
2. 要求一个对象能有不同的表现,并希望将对象的构造与表现解耦。
3. 想要在某个时间点创建对象,但在稍后的时间点再访问

③.原型模式

简介:
原型设计模式(Prototype design pattern)帮助我们创建对象的克隆,其最简单的形式就是一个clone函数,接受一个对象作为输入参数,返回输入对象的一个副本。在Python中,这可以使用copy.deepcopy()函数来完成。

现实生活的例子:
原型设计模式无非就是克隆一个对象。有丝分裂,即细胞分裂的过程,是生物克隆的一个例子。
另一个著名的(人工)克隆例子是多利羊

软件的例子:
很多Python应用都使用了原型模式,但几乎都不称之为原型模式,因为对象克隆是编程语言的一个内置特性。
.
可视化工具套件(Visualization Toolkit,VTK)是原型模式的一个应用。VTK是一个开源的跨平台系统,用于三维计算机图形/图片处理以及可视化。VTK使用原型模式来创建几何元素(比如,点、线、六面体等)的克隆。
.
music21也是使用原型模式的项目。根据该项目页面所述,“music21是一组工具,帮助学者和其他积极的听众快速简便地得到音乐相关问题的答案”。 music21工具套件使用原型模式来复制音符和总谱。

应用案例:
当我们已有一个对象,并希望创建该对象的一个完整副本时,原型模式就派上用场了。在我们知道对象的某些部分会被变更但又希望保持原有对象不变之时,通常需要对象的一个副本。在这样的案例中,重新创建原有对象是没有意义的。
.
另一个案例是,当我们想复制一个复杂对象时,使用原型模式会很方便。对于复制复杂对象,我们可以将对象当作是从数据库中获取的,并引用其他一些也是从数据库中获取的对象。若通过多次重复查询数据来创建一个对象,则要做很多工作。在这种场景下使用原型模式要方便得多。

代码实现

from collections import OrderedDict
import copy

class Book:
    """
    Book类展示了一种有趣的技术可避免可伸缩构造器问题。在__init__() 方法中,仅有三个形参是固定的:
    name、authors和price,但是使用rest变长列表,调用者 能以关键词的形式(名称=值)传入更多的参数。
    self.__dict__.update(rest)一行将rest 的内容添加到Book类的内部字典中,成为它的一部分。
    """

    def __init__(self, name, authors, price, **rest):
        '''rest的例子有: 出版商、长度、 标签、出版日期等等'''
        self.name = name
        self.authors = authors
        self.price = price  # 单位为美元
        self.__dict__.update(rest)

    def __str__(self):
        """
        我们并不知道所有被添加参数的名称,但又需要访问内部字典将这些参数 应用到__str__()中,并且字典的内容并不遵循
        任何特定的顺序,所以使用一个OrderedDict来强制元素有序,否则,每次程序执行都会产生不同的输出。
        :return: str
        """
        mylist = []
        ordered = OrderedDict(sorted(self.__dict__.items()))
        for i in ordered.keys():
            mylist.append('{}: {}'.format(i, ordered[i]))
            if i == 'price':
                mylist.append('$')
            mylist.append('\n')
        return ''.join(mylist)

class Prototype:
    """
    Prototype类实现了原型设计模式。Prototype类的核心是clone()方法,该方法使用我们 熟悉的copy.deepcopy()
    函数来完成真正的克隆工作。但Prototype类在支持克隆之外做了一 点更多的事情,它包含了方法register()和unregister(),
    这两个方法用于在一个字典中追 踪被克隆的对象。注意这仅是一个方便之举,并非必需。
    """

    def __init__(self):
        self.objects = dict()

    def register(self, identifier, obj):
        self.objects[identifier] = obj

    def unregister(self, identifier):
        del self.objects[identifier]

    def clone(self, identifier, **attr):
        """
        clone()方法和Book类中的__str__使用了相同的技巧,但这次是因为别的原因。使 用变长列表attr,
        我们可以仅传递那些在克隆一个对象时真正需要变更的属性变量
        :param identifier:版本号
        :param attr: 新添加的属性
        :return: obj
        """
        found = self.objects.get(identifier)

        if not found:
            raise ValueError('Incorrect object identifier: {}'.format(identifier))
        obj = copy.deepcopy(found)
        obj.__dict__.update(attr)
        return obj

def main():
    """
    main()函数以实践的方式展示了本节开头提到的《C程序设计语言》一书克隆的例子。克隆 该书的第一个版本来创建第二个版本,
    我们仅需要传递已有参数中被修改参数的值,但也可以传递额外的参数。在这个案例中,edition就是一个新参数,
    在书的第一个版本中并不需要,但对 于克隆版本却是很有用的信息。
    :return:
    """
    b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'), price=118,
              publisher='Prentice Hall', length=228, publication_date='1978-02-22',
              tags=('C', 'programming', 'algorithms', 'data structures'))

    prototype = Prototype()
    cid = 'k&r-first'
    prototype.register(cid, b1)
    b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99, length=274,
                         publication_date='1988-04-01', edition=2)

    for i in (b1, b2):
        print(i)
    print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))

if __name__ == '__main__':
    main()

原型模式的总结:
深副本与浅副本。
深副本:原始对象的所有数据都被简单地复制到克隆对象中,没有例外。
浅副本:则依赖引用,我们可以引入数据共享和写时复制一类的技术来优化性能(例如,减小克隆对象的创建时间)和内存使用。如果可用资源有限(例如,嵌入式系统)或性能至关重 要(例如,高性能计算),那么使用浅副本可能更佳。
.
在Python中,可以使用copy.copy函数进行浅复制。以下内容引用自Python官方文档,说明了浅副本copy.copy和深副本(copy.deepcopy())之间的区别。
浅副本构造一个新的复合对象后,(会尽可能地)将在原始对象中找到的对象的引用插入新对象中。
深副本构造一个新的复合对象后,会递归地将在原始对象中找到的对象的副本插入新对象中。
.
原型模式用于创建对象的完全副本。确切地说,创建一个对象的副本可以指代以下两件事情。
第一种:当创建一个浅副本时,副本依赖引用
第二种:当创建一个深副本时,副本复制所有东西
.
第一种情况中:
我们关注提升应用性能和优化内存使用,在对象之间引入数据共享,但需要小心地修改数据,因为所有变更对所有副本都是可见的。浅副本在本章中没有过多介绍,但也许 你会想试验一下。
第二种情况中:
我们希望能够对一个副本进行更改而不会影响其他对象。对于我们之前看到的蛋糕食谱示例这类案例,这一特性是很有用的。这里不会进行数据共享,所以需要关注因对象 克隆而引入的资源耗用问题。

上一篇:SQL基础知识普及(二十二)——IS NULL 和 IS NOT NULL


下一篇:DBA需要考虑备份相关问题