Python设计模式巡礼1:创建型模式

本文参考《设计模式——可复用面向对象软件基础》

完整代码请看: https://github.com/ubwshook/PythonStudy/tree/master/design_patterns

谈及设计模式就不得不的提起《设计模式——可复用面向对象软件基础》,这是一部设计模式的经典书籍,它归纳了常用的23中设计模式。设计模式使人们可以更加简单和方便地复用成功的设计和体系结构。 该书中主要是采用C++作为示例语言,虽然设计模式是一种编程思想,但是不同语言所提供的特性会让设计模式的实现非常的不同。Python的一等函数以及其他动态特性都会使设计模式的实现产生变化。

我们将对书中提到设计模式使用Python语言进行分析重构,所使用的例子尽可能与书中相同,下面让我们一起开始Python设计模式巡礼的第一站——创建型模式!

依据设计模式的目的可分为 创建型(Creational)、结构型(Structural)、行为型(Beaviroal) 三种。创建型模式与对象的创建有关; 结构型模式处理类或对象的组合; 行为型对类或对象怎么交互和怎样分配职责进行描述。

0x0 . 迷宫问题描述以及初始Dome

迷宫使我们都玩过的一种游戏,我们现在来关注一个迷宫怎么创建。我们将一个迷宫定义为一系列的房间,一个房间要知道他的邻居;可能的邻居要么是另一个房间,要么是一堵墙、或者是到另一个房间的一扇门。类Room、Door、Wall定义了我们所有的例子中使用到的构建。

首先我们定义一个MapSite类:

class MapSite(object):
    """
    类MapSite是所有迷宫组件的公共抽象类。我了简化例子,MapSite仅定义了一个操作Enter,它取决于你在进入什么。
    如果你进入一个房间,那么你的位置会发生改变。如果你试图进入一扇门,那么这两件事中就有一件会发生:如果们是开
    着的,你进入另一个房间。如果门是关着的,那么你就会碰壁。
    """
    def enter(self):
        """
        Enter是为更加复杂的有些操作提供了一个简单基础。例如,如果你在一个房间中说“向东走”,
        游戏只能确定直接在东边是哪一个MapSite并对它调用Enter。特定子类的Enter操作将计算
        出你的位置是发生改变,还是碰壁。
        :return:
        """
        return

Room是MapSite的一个子类,用来定义房间:

class Room(MapSite):
    """
    Room是MapSite的一个具体子类,而MapSite定义了迷宫中构件之间的主要关系。
    Room是指向其他MapSite对象的引用,并保存一个房间号,用来标识迷宫中的房间
    """
    def __init__(self, room_no):
        super(Room, self).__init__()
        self._room_no = room_no
        self._sides = {'East': None, 'West': None, 'North': None, 'South': None }

    def get_side(self, direction):
        return self._sides[direction]

    def set_side(self, direction, map_site):
        if direction not in ['East', 'West', 'North', 'South']:
            print("Dirction is invalid!")

        self._sides[direction] = map_site

    def get_no(self):
        return self._room_no

Room中初始化的时候会设置房间号。我们可以使用set_side和get_side来设置或获取房间某一面的对象。

定义Wall类,Wall来表示墙,墙这里比较简单没有额外的东西:

class Wall(MapSite):
    """
    这个类描述的是墙,在demo中是比较简单的
    """
    def __init__(self):
        super(Wall, self).__init__()

定义Door类,Door表示一扇门,需要知道门联通了那两个房间:

class Door(MapSite):
    """
    这个类描述的是门这个对象
    """
    def __init__(self, room1: Room, room2: Room):
        super(Door, self).__init__()
        self._room1 = room1
        self._room2 = room2
        self._is_open = 0

    def other_side_from(self, room):
        """
        获取一个房间另一面的房间
        :param room:
        :return: 输入room另一面的room
        """
        if room.get_no() == self._room1.get_no():
            return self._room2
        elif room.get_no() == self._room2.get_no() :
            return self._room1
        else:
            print("Room is wrong!")
            return None

定义一个Maze类,用来表示房间的集合。可以从中按照房间号去获取room对象:

class Maze(object):
    """
    迷宫中房间集合的类,可以向迷宫中添加或者获取房间
    """
    def __init__(self):
        self.rooms = {}

    def add_room(self, room: Room):
        self.rooms[room.get_no()] = room

    def get_room(self, room_no):
        return self.rooms[room_no]

我们的最终目标是为迷宫游戏创建迷宫,这里有一个迷宫游戏类MazeGame,其中crete_maze函数是创建一个迷宫的方法, 简单创建一个迷宫如下, 创建两个房间,两个房间用door连接,这是一个非常简单迷宫。

class MazeGame(object):
    """
    Maze游戏类,没有完整去实现功能,我们主要关注创建型模式,所以这里只描述创建函数的实现。
    """
    @staticmethod
    def create_maze():
        maze = Maze()
        room1 = Room(1)
        room2 = Room(2)
        door = Door(room1, room2)

        maze.add_room(room1)
        maze.add_room(room2)

        room1.set_side('North', Wall())
        room1.set_side('East', door)
        room1.set_side('South', Wall())
        room1.set_side('West', Wall())

        room2.set_side('North', Wall())
        room2.set_side('East', Wall())
        room2.set_side('South', Wall())
        room2.set_side('West', door)

        return maze

对于一个仅有两个房间的迷宫来说,这套代码是相当的复杂的。而它真正的问题是不灵活,它对迷宫的布局采用了硬编码,这意味着改变布局,就要改变这个成员函数。

考虑另一种情况,你想设计一个试了魔法的迷宫,这里面的Room、Wall、Door等组件都与基础的不同,那么怎样跟容易改变create_maze以让它用这些新类型的对象创建迷宫。

创建型的设计模式将根据场景的不同进行实现。

0x1 . ABSTRACT FACTORY(抽象工厂)

1.意图

提供一个创建一系列相关或者相互依赖对象的接口,而无需指定它们具体的类。

2.别名

Kit

3.适用性:

  • 一个系统要独立与它的产品创建、组合和表示时。
  • 一个系统要由多个产品系列中的一个来配置时。
  • 当你要强调一些列相关产品对象的设计以便进行联合使用时。
  • 当你提供一个产品类库,而只是想显示它们的接口而不是实现时。

4.实现概要

将工厂作为单件,为创建函数设计接口,用于接收工厂实例为创建参数。工厂类可以被继承,从而实现使用不同组件来进行创建。

5.效果:

  • 它分离了具体的类:因为一个工厂封装了创建对象的责任和过程,它将客户与类的实现分离。
  • 它使得替换产品系列变得很容易:只要改变工厂类,就可以更换产品系列。
  • 有利于产品的一致性:一个应用只能使用同一系列中的对象。
  • 难以支持新种类的产品:抽象工厂确定了可以被创建的组件的集合,如果要支持新的组件的创建,就需要修改工厂接口,可能影响多有子类的改变。

5.代码例示:

依然是迷宫问题,我们先设计一个MazeFactory,这个工厂类将提供各个组件的创建方法:

class MazeFactory(object):
    """
    工厂类,定义各个组件如何生成
    可以被覆写,定制不同工厂类
    """
    def make_maze(self):
        return Maze()

    def make_wall(self):
        return Wall()

    def make_door(self, room1: Room, room2: Room):
        return Door(room1, room2)

    def make_room(self, room_no: int):
        return Room(room_no)

为了使用factory我们的MazeFactory的create_maze方法将factory作为参数,进行迷宫的创建:

class MazeGame(object):
    """
    Maze游戏类, 这里设计的create_maze函数可以接收工厂对象进行类型初始化。
    """
    def create_maze(self, factory):
        maze = factory.make_maze()
        room1 = factory.make_room(1)
        room2 = factory.make_room(2)
        door = factory.make_door(room1, room2)

        maze.add_room(room1)
        maze.add_room(room2)

        room1.set_side('North', factory.make_wall())
        room1.set_side('East', door)
        room1.set_side('South', factory.make_wall())
        room1.set_side('West', factory.make_wall())
        room2.set_side('North', factory.make_wall())
        room2.set_side('East', factory.make_wall())
        room2.set_side('South', factory.make_wall())
        room2.set_side('West', door)
        
        return maze()

我们可以设计自己的工程类,从而定制自己的迷宫组件,比如我想要将迷宫的组件改为施加魔法的门或房间的时候,我就可以继承MazeFactory类,覆写里面生成组件的方法。

class EnchantedMazeFactory(MazeFactory):
    """
    用于创建施加了魔法迷宫的工厂类,可以生成施加魔法的
    """
    def make_door(self, room1: Room, room2: Room):
        return EnchantedDoor(room1, room2)

    def make_room(self, room_no: int):
        return EnchantedRoom(room_no)

去创建一个施加了魔法的迷宫就变得很容易:

# 创建一个施加了魔法的迷宫
game = MazeGame()
game.create_maze(EnchantedMazeFactory())

0x2 . BUILDER(生成器)——对象创建型模型

1.意图

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

2.适用性

  • 当创建复杂对象的算应该独立于该对象组成部分以及他们的装配方式时。
  • 当构造过程必须云讯被构造的对象有不同的表示时。

3.实现概要

builder会把组件的构造封装在自己类内部,对外不体现构建过程,只需要不断生成组件,组件之间的联系,也被封装在builder内部,使用者只需要调用builder的方法去生成即可。

4.效果

  • 它使你可以改变一个产品的内部表示:builder接口隐藏了产品的表示和内部结构,同时也隐藏了该产品是如何装配的。因为产品通过抽象接口构造的,你在改变产品内部表示时所要做的只是定义一个新的生成器。
  • 它将构造代码和表示代码分离:客户不需要知道定义产品内部结构的类的所有信息,
  • 它使你可以对构造过程进行更精细的控制: builder实在导向者的控制下一步步构造产品的,能够更好反应产品的构造过程。这使你可以更加精细的控制构建过程。

5.代码示例:

首先我们创建一定抽象类来定义一个builder必须要实现的方法,任何构造Maze的类都必须继承它并实现对应的方法:

class MazeBuilder(object, metaclass=ABCMeta):
    """
    迷宫生成器的抽象类,用于定义迷宫生成器必须实现的方法
    """
    @abstractmethod
    def build_maze(self):
        pass

    @abstractmethod
    def build_room(self, room_no: int):
        pass

    @abstractmethod
    def build_door(self, room1_no: int, room2_no: int):
        pass

    @abstractmethod
    def get_maze(self):
        pass

接下来我们来具体实现一个builder,这里我们把它称为标准生成器:

class StandardMazeBuilder(MazeBuilder):
    """
    一个标准迷宫生成器,继承于Mazebuilder
    """
    def __init__(self):
        """
        初始化一个字典用于存放迷宫中的房间
        """
        self._current_maze = {}

    def build_maze(self):
        """
        每次调用创建迷宫就会初始化迷宫房间字典
        :return:
        """
        self._current_maze = {}

    def build_room(self, room_no):
        """
        创建一个房间,并将其加入迷宫房间字典中
        :param room_no: 房间编号
        :return:
        """
        if room_no not in self._current_maze.keys():
            room = Room(room_no)
            self._current_maze[room_no] = room

            for direction in DIRECTIONS:
                room.set_side(direction, Wall())

    def build_door(self, room1_no, room2_no):
        """
        为两个房间之间创建一道门,通过_common_wall确定门所在的方位
        :param room1_no: 房间1编号
        :param room2_no: 房间2编号
        :return:
        """
        room1 = self._current_maze[room1_no]
        room2 = self._current_maze[room2_no]
        door = Door(room1, room2)

        room1.set_side(self._common_wall(room1, room2), door)
        room2.set_side(self._common_wall(room2, room1), door)

    def get_maze(self):
        """
        返回迷宫信息
        :return: 返回迷宫字典
        """
        return self._current_maze

    def _common_wall(self, room1: Room, room2: Room):
        """
        假设迷宫宽度为8个房间,如果相邻或者在迷宫中模8相等,才能获得方位。
        :param room1: 房间1编号
        :param room2: 房间2编号
        :return:
        """
        room1_no = room1.get_no()
        room2_no = room2.get_no()

        if room2_no == room1_no + 1 and room1_no % 8 != 0:
            return 'East'

        if room1_no == room2_no + 1 and room2_no % 8 != 0:
            return 'West'

        if room1_no % 8 == room2_no % 8 and room1_no // 8 == room2_no // 8 + 1:
            return 'North'

        if room1_no % 8 == room2_no % 8 and room2_no // 8 == room1_no // 8 + 1:
            return 'South'

        return None

StandardMazeBuilder继承了MazeBuilder并实现了抽象接口方法,整个maze的构建都在类的内部进行,客户只需要添加组件,builder会为用户创建迷宫:

class MazeGame(object):
    """
    迷宫游戏类,这里仅仅实现创建迷宫的方法
    """
    def create_maze(self, builder):
        builder.build_maze()

        builder.build_room(1)
        builder.build_room(2)
        builder.build_door(1, 2)

        return builder.get_maze()

0x3 . FACTORY METHOD(工厂方法)

1.意图:

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。

2.别名:

虚构造器(virtual Constructor)

3.适用性:

  • 当一个类不知道它所必须创建的对象的类的时候。
  • 当一个类希望由它的子类来指定它所创建的对象的时候。
  • 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类时代理者这一信息局部化的时候。

4.效果:

  • 为子类提供挂钩
  • 连接平行类层次

5.实现概要

通俗的说,就是抽象类方法的应用,继承+覆写某些函数。

6.代码示例:

工厂方法模式会再MazeGame类内部定义各组件创建方法,并在创建迷宫函数create_maze中去调用:

class MazeGame(object):
    def make_maze(self):
        return Maze()

    def make_room(self, room_no):
        return Room(room_no)

    def make_door(self, room1: Room, room2: Room):
        return Door(room1, room2)

    def make_wall(self):
        return Wall()

    def create_maze(self):
        maze = self.make_maze()
        room1 = self.make_room(1)
        room2 = self.make_room(2)
        door = self.make_door(room1, room2)

        maze.add_room(room1)
        maze.add_room(room2)

        room1.set_side('North', self.make_wall())
        room1.set_side('East', door)
        room1.set_side('South', self.make_wall())
        room1.set_side('West', self.make_wall())

        room2.set_side('North', self.make_wall())
        room2.set_side('East', self.make_wall())
        room2.set_side('South', self.make_wall())
        room2.set_side('West', door)
        
        return maze

如果想要设计一种具有带炸弹房间和被炸毁的墙的迷宫,需要继承MazeGame()类覆写其中的创建函数。

class BombedMazeGame(MazeGame):
    def make_wall(self):
        return BoomedWall()

    def make_room(self, room_no):
        return RoomWithBomb

0x4 . PROTOTYPE(原型)

1.意图

用原型实例指定创建对象的种类,并通过copy这些原型创建新的对象。

2.适用性:

  • 当实例化的类实在运行时刻指定时,例如,通过动态装载;
  • 为了避免创建一个与产品类层次平行的工厂类层次时;
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆他们可能比每次用合适的状态手工实例化该类方便一些。

3.效果

  • 运行时刻增加或删除产品,运行的时候可以修改原型
  • 改变值以制定新对象
  • 改变结构以指定新对象
  • 减少子类构造
  • 用类动态配置应用

4.实现概要

为每个组件都设置原型,在创建时复制原型即可。

5.代码示例:

我们将构造一个MazePrototypeFactory类,它是一个使用原型模式的工厂类,其中的创建组件的方法均是对原型的copy

class MazePrototypeFactory(MazeFactory):
    """
    使用原型模式的抽象工厂
    """
    def __init__(self, room, wall, door, maze):
        """
        初始化函数设置原型
        :param room: 房间原型
        :param wall: 墙壁原型
        :param door: 门原型
        :param maze: 迷宫原型
        """
        self._prototype_maze = maze
        self._prototype_room = room
        self._prototype_wall = wall
        self._prototype_door = door

    def make_wall(self):
        return deepcopy(self._prototype_wall)

    def make_room(self, room_no):
        """
        利用原型构建房间,这里使用的原型,需要调用初始化之后才能使用,这与之前不同。
        :param room_no: 房间号
        :return:
        """
        room = deepcopy(self._prototype_room)
        room.init(room_no)
        return room

    def make_door(self, room1, room2):
        door = deepcopy(self._prototype_door)
        door.init(room1, room2)
        return door

    def make_make(self):
        return deepcopy(self._prototype_maze)

可以看出其中room、door与之前不同,增加了额外的初始化,所以原先的类定义也有所修改:

class Room(MapSite):
    """
    Room是MapSite的一个具体子类,而MapSite定义了迷宫中构件之间的主要关系。
    Room是指向其他MapSite对象的引用,并保存一个房间号,用来标识迷宫中的房间
    """
    def __init__(self):
        super(Room, self).__init__()
        self._room_no = None
        self._sides = {}
        for direction in DIRECTIONS:
            self._sides[direction] = None

    def init(self, room_no):
        self._room_no = room_no

    def get_side(self, direction):
        return self._sides[direction]

    def set_side(self, direction, map_site):
        if direction not in ['East', 'West', 'North', 'South']:
            print("Dirction is invalid!")

        self._sides[direction] = map_site

    def get_no(self):
        return self._room_no


class Door(MapSite):
    """
    这个类描述的是门这个对象
    """
    def __init__(self):
        super(Door, self).__init__()
        self._room1 = None
        self._room2 = None
        self._is_open = 0

    def init(self, room1, room2):
        self._room1 = room1
        self._room2 = room2

    def other_side_from(self, room):
        """
        获取一个房间另一面的房间
        :param room:
        :return: 输入room另一面的room
        """
        if room.get_no() == self._room1.get_no():
            return self._room2
        elif room.get_no() == self._room2.get_no():
            return self._room1
        else:
            print("Room is wrong!")
            return None

下面我们就用原型工程类,来建立一个迷宫:

game = MazeGame()
# 为工厂添加原型
factory = MazePrototypeFactory(Room(), Wall(), Door(), Maze())
maze = game.create_maze(factory)

0X5 SINGLETON(单例模式)

1.意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

2.适用性:

  • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它
  • 当这个唯一实例应该是通过子类化扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

3.效果

  • 对唯一实例的受控访问
  • 缩小名空间

4.代码示例

这里我们不再使用迷宫作为讨论对象,在《python cookbook》中提供了非常易用的Singleton类, 只要继承它, 就会成为单例。

# python 3 代码实现
class Singleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            # 如果 __instance 不存在,创建新的实例
            self.__instance = super().__call__(*args, **kwargs)
            return self.__instance
        else:
            # 如果存在,直接返回
            return self.__instance


class Spam(metaclass=Singleton):
    def __init__(self):
        print('Creating Spam')


a = Spam()
b = Spam()

print(a is b)  # 这里输出为 True

这里通过元类(metaclass)实现了三件事:

  • 拦截类的创建
  • 修改类的定义
  • 返回修改后的类

这个Singleton的元类使用了__call__方法使其能够模拟函数的行为, 例子中我们构造了一个Singleton元类,并使用call方法使其能够模拟函数的行为。构造类 Spam 时,将其元类设为Singleton,那么创建类对象 Spam 时,会走到Singleton的__call__方法中。
Singleton元类仅仅会初始化一次,这样继承它的类,就具有了单例模式。

单例模式还有其他一些实现方法,我们不在这里一一赘述。

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

推荐阅读更多精彩内容