設計模式在Python中的完美實現

一切如來心祕密發表於2020-12-02

Python設計模式

1.0 前言

得益於Python的鴨子型別設計原則,Python相比於 C++ 和 Java 在實現設計模式上更加靈活,甚至可以輕鬆的將多種設計模式組合出更加強大的組合模式。

文章不會依照常見書籍的做法實現設計模式,而是編寫出更加Pythonic的設計模式。

主要包括如下幾個主題:

  • 設計模式——元素
  • 設計模式分類
  • Python模式——建立模式
    • 單例模式
    • 工廠模式
    • 原型模式
    • 建造者模式
  • Python模式——結構化模式
    • 介面卡模式
    • 外觀模式
    • 代理模式
  • Python模式——行為模式
    • 迭代器模式
    • 觀察者模式
    • 狀態模式

1.1 設計模式——元素

首先了解下設計模式的通用元素

設計模式是用來解決物件導向系統中一個或一類問題重複設計。

幾乎所有的設計模式都有以下元素:

  • 名字(name):描述模式的標題。

    比如觀察者模式,一個好的標題可以讓人見名知意。

  • 上下文(context):問題出現的情景

    前邊說了設計模式是解決一類問題的,既然是問題肯定就會有遇到該問題的情景,就是上下文。

  • 問題(problem):需要解決的問題。

    可以從以下幾個方面描述一個問題:

    • 需求(requirement):解決方案要滿足的需求。比如,釋出者 - 訂閱者模式的實現必須支援HTTP。
    • 約束(constraint):解決方案的約束(如果有)。比如,可擴充套件的對等釋出者模式不應該交換三個以上的訊息來發布通知。
    • 性質(property):解決方案想要的一些性質。比如,該解決方案要在不同的作業系統通用。
  • 解決方案(solution):問題的實際解決方案。

    包括元素的結構和職責、靜態關係以及執行時的互動(協作)

    還包括解決問題的程度,和沒有解決的程度,還應當儘量提及它的後果,即應用一個設計模式的結果和權衡

注:一種設計模式幾乎不可能完全解決導致問題的所有因素,但卻讓其中一些因素對相關或替代的實現開放。

1.2 設計模式分類

根據解決問題的型別,設計模式被劃分為三種,當然隨著語言的發展,以後可能會出現新的解決新的問題的方法,這些方法被重複使用,進而出現新的設計模式種類:

  • 建立模式:主要解決物件建立和初始化相關的問題。
    • 工廠模式:確保以可重複可預測的方式建立相關類的例項。
    • 原型模式:通過複製物件來建立相似的物件。
    • 單例模式:類例項化時只被建立初始化一次。
    • 相關模式:確保一個類的任何例項能夠共享相同的初始狀態。
  • 結構化模式:將物件往有意義的結構中組合和組裝,提供可重複使用的行為。
    • 代理模式:通過上層行為的包裝層(wrapper)控制對一個物件及其方法的訪問。
    • 組合模式:使用一個類表示一個由許多元件同時組成的物件。
  • 行為模式:解決由物件的執行時互動引起的問題,以及它們如何分配責任的問題。
    • 中介者模式:確保所有物件在執行時使用鬆耦合方式來相互引用。
    • 觀察者模式:當資源的狀態發生變化時,物件希望被通知,但是不希望保持輪詢資源來發現改變(系統中可能有很多這樣的物件例項)。

設計模式為什麼被劃分為三種?

  • 我們以物件導向的思維思考一下一個系統的生命週期:

    • 初期:系統中的各個物件被初始化建立,這個時候會出現需要一些定製化的建立:比如說重複建立問題,物件共享狀態等問題。

      此時就出現了建立模式,解決物件初始化建立的問題。

    • 中期:當各個物件被建立出來後,會出現物件過多或者過於複雜、獨立單一等問題,導致難以利用這些物件去構成系統中的組織。

      此時就出現了結構化模式,去解決物件組合和組裝,提供系統使用的組織行為的問題。

    • 後期:當有了系統的組織行為後,系統基本上可以執行,但是此時又出現了新的問題:組織行為越來越多,並且行為之間的互動也越來越麻煩。

      此時就出現了行為模式,解決物件的執行時互動引起的問題,以及它們如何分配責任的問題

可以發現建立模式、結構化模式和行為模式的順序隱式地嵌入在一個系統執行時物件的生命週期中。物件首先被建立,然後被組合成有用的結構,再然後它們進行互動。

1.3 Python模式——建立模式

單例模式

要求

  • 一個類必須只有一個例項,該例項可以通過一個公開的訪問點訪問。
  • 該類在繼承擴充套件後任然符合單例模式。

實現

  1. 重寫Python的魔法方法__new__可以實現單例模式:
def test_single(cls):
    """測試是否為單例類"""
    return cls() == cls()

class Singleton:
    """單例類"""
    _instance = None

    def __new__(cls):
        if cls._instance == None:
            cls._instance = object.__new__(cls)
        return cls._instance

if __name__ == "__main__":
    print(test_single(Singleton)) # true

繼承Singleton類的類也符合單例模式:

class SingletonA(Singleton):
    """繼承單例類的類也是單例類"""
    pass

if __name__ == "__main__":
    print(test_single(SingletonA))	# true
  1. 定義一個單例元類,重寫該類的__call__方法,也可以實現單例類。
class MetaSingleton(type):
    """單例元類, 繼承該類的所有類都是單例類"""
    def __init__(cls, *args):
        print(cls, "__init__ method called with args", args)
        type.__init__(cls, *args)
        cls.instance = None
    
    def __call__(cls, *args, **kwargs):
        if not cls.instance:
            print(cls, "creating instance", args, kwargs)
        return cls.instance


class SingletonM(metaclass=MetaSingleton):
    """繼承單例元類的頂級單例類,後續繼承該類的類都是單例類"""
    pass


if __name__ == "__main__":
    print(test_single(SingletonM))

共享初始狀態

類必須為所有例項提供一種共享相同初始狀態的方法。確保在特定記憶體位置只有一個例項存在的技術只是實現此目的的一種方法。

拋開教科書式地設計模式,可以使用更加pythonic的方式實現共享。

class Borg:
    """非單例實現初始狀態共享"""
    __shared_state = {}

    def __init__(self):
        self.__dict__ = self.__shared_state

class IBorg(Borg):
    """共享Borg類初始狀態"""

    def __init__(self):
        Borg.__init__(self)
        self.state = 'init'

    def __str__(self):
        return self.state
>>> i1 = IBorg()
>>> i2 = IBorg()
>>> print(i1) 
init
>>> print(i2) 
init
>>> print(i1.name) 
zzy
>>> print(i2.name) 
zzy
>>> print(i1==i2) 
False
>>>

Borg共享類與單例類比較

  • 先看下單例類在繼承後共享狀態情況,可以發現在繼承的兄弟類中並沒有共享狀態:

    >>> class SingletonA(Singleton): pass
    ... 
    >>> class SingletonB(Singleton): pass 
    ... 
    >>> class SingletonA1(SingletonA): pass 
    ... 
    >>> a = SingletonA()
    >>> a1 = SingletonA1() 
    >>> b = SingletonB()   
    >>> a.x = 100
    >>> print(a.x) 
    100
    >>> print(a1.x) 
    100
    >>> print(b.x)  
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'SingletonB' object has no attribute 'x'
    
  • 看下共享類Borg的共享狀態情況,可以發現繼承Borg共享類的所有子類都共享了Borg類的狀態:

    >>> class ABorg(Borg): pass
    ... 
    >>> class BBorg(Borg): pass 
    ... 
    >>> class A1Borg(Borg): pass 
    ... 
    >>> a = ABorg()
    >>> b = BBorg() 
    >>> a1 = A1Borg() 
    >>> a.x = 100
    >>> print(a1.x) 
    100 
    >>> print(a.x)  
    100 
    >>> print(b.x) 
    100 
    

工廠模式

要求

  • 通過類的入口點將不同的引數傳遞給工廠方法,返回不同的例項物件。

實現

  1. 定義一個員工的抽象基類:初始化員工的資訊,定義一個定義角色的抽象基類方法。
  2. 定義不同的角色類繼承抽象基類,重寫抽象基類方法定義角色。
  3. 定義產生員工的工廠類:定義一個工廠方法接受引數,返回不同的員工例項物件。
from abc import ABCMeta, abstractclassmethod

class Employee(metaclass=ABCMeta):
    """員工抽象基類"""

    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    @abstractclassmethod
    def get_role(self):
        pass

    def __str__(self):
        return f"{self.__class__.__name__} - {self.name}, {self.age} years old {self.gender}"
    

class Engineer(Employee):
    """繼承抽象基類,重寫抽象方法: 定義員工"""

    def get_role(self):
        return "engineering"


class Accountant(Employee):
    """繼承抽象基類,重寫抽象方法: 定義員工"""

    def get_role(self):
        return "accountant"


class Admin(Employee):
    """繼承抽象基類,重寫抽象方法: 定義員工"""

    def get_role(self):
        return "administration"


class EmployeeFactory:
    """定義員工工廠類"""

    @classmethod
    def create(cls, name, *args):
        """生成員工例項的工廠方法"""
        name = name.lower().strip()

        if name == 'engineer':
            return Engineer(*args)
        elif name == 'accountant':
            return Accountant(*args)
        elif name == 'admin':
            return Admin(*args)
>>> factory = EmployeeFactory()

>>> print(factory.create('engineer', 'Sam', 25, 'M'))
Engineer - Sam, 25 years old M

>>> print(factory.create('accountant', 'ZZY', 22, 'M')) 
Accountant - ZZY, 22 years old M

>>> admin = factory.create('admin', 'Superli', 32, 'F')
>>> print(admin)
Admin - Superli, 32 years old F

>>> admin.get_role()
'administration'

# 因為工廠方法定義為了類方法,我們可以無需例項化工廠類就產生員工
>>> print(EmployeeFactory.create('admin', 'ZZY', 32, 'M')) 
Admin - ZZY, 32 years old M

抽象基類中的抽象方法一般不寫內容,但是繼承該類的子類必須重寫抽象方法實現具體內容。將工廠類保留的介面方法定義為類方法,可以在直接通過工廠類去呼叫該方法而無需去例項化化一個工廠類物件。這在有時候可能會比較方便。

原型模式

要求

  • 以一個類的例項作為模板例項,通過複製或克隆此原型來建立新的例項。

實現

  1. 使用深度複製實現

    import copy
    
    class Prototype(object):
        """
        原型基類
        """
        def clone(self):
            return copy.deepcopy(self)
    
    class Register(Prototype):
        """
        一個學生註冊類
        """
        def __init__(self, names=[]):
            self.names = names
    
    >>> r1 = Register(names=['bobo', 'jack', 'rose']) 
    >>> r2 = r1.clone()
    >>> print(r1) 
    <__main__.Register object at 0x000001B2F3BA9198>
    >>> print(r2) 
    <__main__.Register object at 0x000001B2F3BA92B0>
    >>> r2.__class__
    <class '__main__.Register'>
    >>> r2.names.append('zzy') 
    >>> r2.names
    ['bobo', 'jack', 'rose', 'zzy']
    >>> r1.names 
    ['bobo', 'jack', 'rose']
    
  2. 使用元類的原型

    import copy
    
    class MetaPrototype(type):
           """
           實現原型模式的元類
           """
           def __init__(cls, *args):
               type.__init__(cls, *args)
               cls.clone = lambda self: copy.deepcopy(self)
    
    
       class PrototypeM(metaclass=MetaPrototype):
           """
           繼承原型元類的原型模式基類
           """
           pass
    
    
       class ItemCollection(PrototypeM):
           """
           一個列表容器原型類
           """
           def __init__(self, items=[]):
               self.items = items
    
    i1 = ItemCollection(items=['apples', 'bananas', 'oranges'])
    i2 = i1.clone()
    i1 
    <__main__.ItemCollection object at 0x0000018D57207240>
    i2
    <__main__.ItemCollection object at 0x0000018D57207390>
    i1.items is i2.items 
    false
    
  3. 使用元類來合成模式

    使用元類可以將看似衝突的設計模式合成為新的設計模式。現在使用元類將單例模式和原型模式合併,使其既有單例模式的特性,又有原型模式的特性。

    import copy
    
    class MetaSingletonPrototype(type):
        """單例和原型合併的元類"""
    
        def __init__(cls, *args):
            print(cls, "__init__ method called with args", args)
            type.__init__(cls, *args)
            cls.instance = None
            cls.clone = lambda self: copy.deepcopy(cls.instance)
    
        def __call__(cls, *args, **kwargs):
            if not cls.instance:
                print(cls, "creating prototypeical instance", args, kwargs)
                cls.instance = type.__call__(cls, *args, **kwargs)
            return cls.instance
    
    
    class PrototypeM(metaclass=MetaSingletonPrototype):
        pass
    
    class ItemCollection(PrototypeM):
        def __init__(self, items=[]):
            self.items = items 
    
    >>> i1 = ItemCollection(items=['apples', 'bananas', 'oranges']) 
    <class '__main__.ItemCollection'> creating prototypeical instance () {'items': ['apples', 'bananas', 'oranges']}
    
    >>> i2 = i1.clone()
    
    >>> i1
    <__main__.ItemCollection object at 0x0000021151AA9400>
    >>> i2
    <__main__.ItemCollection object at 0x0000021151AA94A8>
    
    >>> i2.items.append('zzy') 
    >>> i1.items
    ['apples', 'bananas', 'oranges']
    
    >>> i2.items 
    ['apples', 'bananas', 'oranges', 'zzy']
    
    >>> i1.items is i2.items
    False
    
    >>> i3 = ItemCollection(items=['a', 'b', 'c'])
    >>> i3 is i1
    True
    
  4. 原型工廠

    • 將相關模式與工廠模式結合,建立具有共享初始狀態的物件。
    • 使用原型模式來實現克隆
    • 結合上邊實現一個登錄檔的功能:註冊後的例項物件可以克隆
    import copy
    
    
    class Sprototype:
        """原型類基類"""
        def clone(self):
            """返回深度複製的self"""
            return copy.deepcopy(self)
    
    class Borg:
        """共享初始狀態基類"""
        _shared_state = {}
        def __init__(self):
            self.__dict__ = self._shared_state
    
    class PrototypeFactory(Borg):
        """擁有共享初始狀態的工廠註冊類"""
    
        def __init__(self):
            self._registry = {}
    
        def registry(self, instance):
            """註冊給定例項"""
            self._registry[instance.__class__] = instance
    
        def clone(self, klass):
            """返回給定註冊後的類的克隆後的例項"""
            instance = self._registry.get(klass)
            if instance == None:
                print('Error:', klass, 'not registered')
            else:
                return instance.clone()
    
    
    class Name(Sprototype):
        """名稱原型類"""
        def __init__(self, first, second):
            self.first = first
            self.second = second    
    
        def __str__(self):
            return ' '.join((self.first, self.second))
        
    class Animal(Sprototype):
        """動物原型類"""
        def __init__(self, name, a_type='Wild'):
            self.name = name
            self.type = a_type
    
        def __str__(self):
            return ' '.join((str(self.type), self.name))
    
    >>> name = Name('Bill', 'Bryson') 
    >>> animal = Animal('Elephant') 
    >>> print(name) 
    Bill Bryson
    >>> print(animal) 
    Wild Elephant
    
    >>> # 例項化原型工廠物件
    ... 
    >>> factory = PrototypeFactory()
    
    # 註冊物件
    >>> factory.registry(animal) 
    >>> factory.registry(name)   
    
    >>> # 註冊完畢例項物件後,克隆任意數量的例項
    ... 
    >>> factory.clone(Name) 
    <__main__.Name object at 0x000001504B6F9908>
    >>> factory.clone(Animal) 
    <__main__.Animal object at 0x000001504B6F9978>
    
    >>> # 克隆沒有註冊的例項會提示未註冊
    ... 
    >>> class C: pass
    ... 
    >>> factory.clone(C)
    Error: <class '__main__.C'> not registered
    >>>
    

建造者模式

要求

  • 將物件的構造和組裝分離,確保可以使用相同的構建過程構建不同的表現形式。

實現

  • 使用建造者模式來建造擁有不同房間數量和門廊數量等配置的房屋

  • 通過封裝原始的建造者類,可以更加個性化的、方便的定製不同配置的建造者類

    
    
    class Room:
        """房間類:窗戶和門"""
        def __init__(self, nwindows=2, doors=1, direction='S'):
            self.nwindows = nwindows
            self.doors = doors
            self.direction = direction
    
        def __str__(self):
            return "Room <facing:%s, windows=#%d>" % (self.direction,
                                                      self.nwindows)
    
    
    class Porch:
        """門廊類:門"""
        def __init__(self, ndoors=2, direction='W'):
            self.ndoors = ndoors
            self.direction = direction
    
        def __str__(self):
            return "Porch <facing:%s, doors=#%d>" % (self.direction,
                                                    self.ndoors)
        
    
    class LegoHouse:
        """ 搭建的整個房屋類:房間和門廊 """
        def __init__(self, nrooms=0, nwindows=0, nporches=0):
            self.nwindows = nwindows
            self.nporches = nporches
            self.nrooms = nrooms
            self.rooms = []
            self.porches = []
    
        def __str__(self):
            msg="LegoHouse<rooms=#%d, porches=#%d>" %(
                self.nrooms,
                self.nporches
            )
    
            for i in self.rooms:
                msg += str(i)
    
            for i in self.porches:
                msg += str(i)
    
            return msg
    
        def add_room(self, room):
            """ 給整件房屋增加一個房間 """
            self.rooms.append(room)
    
        def add_porch(self, porch):
            """ 給整件房屋增加一個門廊 """
            self.porches.append(porch)
    
    
    class LegoHouseBuilder:
        """ 房屋建造者 """
        def __init__(self, *args, **kwargs):
            self.house = LegoHouse(*args, **kwargs)
    
        def build(self):
            """ 構造一個房屋例項並返回 """
            self.build_rooms()
            self.build_porches()
            return self.house
    
        def build_rooms(self):
            """ 構建房間方法 """
            for i in range(self.house.nrooms):
                room = Room(self.house.nwindows)
                self.house.add_room(room)
        
        def build_porches(self):
            """ 構建門廊方法 """
            for i in range(self.house.nporches):
                porch = Porch(1)
                self.house.add_porch(porch)
    
    if __name__ == "__main__":
        builder = LegoHouseBuilder(nrooms=2, nporches=1, nwindows=1)
        print(builder.build())
    
        class SmallLegoHouseBuilder(LegoHouseBuilder):
            """ 將常用的建造者配置封裝為子類 """
            def __init__(self):
                self.house = LegoHouse(nrooms=2, nporches=1, nwindows=2)
    
        # 使用封裝的建造者配置使用起來更加方便
        small_house = SmallLegoHouseBuilder().build()
        print(small_house)
    
        # 封裝一個建造面向北方的房屋類
        class NorthFacingHouseBuilder(LegoHouseBuilder):
            """ 面向北方的房屋建造者類 """
            def build_rooms(self):
                for i in range(self.house.nrooms):
                    room = Room(self.house.nwindows, direction='N')
                    self.house.add_room(room)
            
            def build_porches(self):
                for i in range(self.house.nporches):
                    porch = Porch(1, direction='N')
                    self.house.add_porch(porch)
    
        print(NorthFacingHouseBuilder(nrooms=2, nporches=1, nwindows=1).build())
    
    LegoHouse<rooms=#2, porches=#1>Room <facing:S, windows=#1>Room <facing:S, windows=#1>Porch <facing:W, doors=#1>
    LegoHouse<rooms=#2, porches=#1>Room <facing:S, windows=#2>Room <facing:S, windows=#2>Porch <facing:W, doors=#1>
    LegoHouse<rooms=#2, porches=#1>Room <facing:N, windows=#1>Room <facing:N, windows=#1>Porch <facing:N, doors=#1>
    
  • 注意

    • 建造者模式中具體的執行動作(如:add_room, add_porch)應該放在需要構造的物件的類中(LegoHouse),而不是放在建造者類中(LegoHouseBuilder)。

      建造者類(LegoHouseBuilder)負責的是對執行動作的呼叫(build_rooms、build_porches)和組裝(build)。

      所以一般建造者類在初始化方法中就會獲取構造物件(self.house = LegoHouse(*args, **kwargs)),然後在呼叫和組裝的過程中使用該物件呼叫具體的執行動作。

    • 建造者模式與工廠模式的區別

      兩種模式有相似的地方:都是傳遞不同的入參最終生成不同的物件。

      但是兩者有本質上的區別:

      1. 工廠模式重點強調的是入參的不同創造不同的物件,也就是說無中生有,強調創造。
      2. 建造者模式重點強調的是執行動作的不同組合出不同的物件,重點強調組合。

1.4 Python模式——結構化模式

介面卡模式

需求

  • 將現有介面包裝或調整為另一個期望的介面。

實現

  • 將一個多邊形類適配為不同形狀的類,並提供預期的介面。

  • 有兩種實現方式,一種是通過繼承基類,實現預期介面方法的方式。一種是通過在例項化基類物件,通過物件組合的方式,即物件介面卡。

  • 繼承基類的方式

    多邊形基類:

    class Polygon:
        """ 多邊形基類 """
        def __init__(self, *sides):
            """ 邊的數量 """
            self.sides = sides
    
        def perimeter(self):
            """ 周長 """
            return sum(self.sides)
    
        def is_valid(self):
            """ 判斷是否合法 """
            raise NotImplementedError
    
        def is_regular(self):
            """ 判斷是否為規則多變形 """
            side = self.sides[0]
            return all([x==side for x in self.sides[1:]])
    
        def area(self):
            """ 計算並返回面積 """
            raise NotImplementedError
    

    使用介面卡模式實現三角形類,實現預期介面為is_equilateral,is_isosceles

    class Triangle(Polygon):
        """ 使用介面卡模式實現三角形類 """
        def is_equilateral(self):
            """ 判斷是否等邊 """
            if self.is_valid():
                return super(Triangle, self).is_regular()
        
        def is_isosceles(self):
            """ 判斷是否為等腰三角形 """
            if self.is_valid():
                for a,b in itertools.combinations(self.sides, 2):
                    if a == b:
                        return True
            return False
    
        def area(self):
            """ 計算面積 """
            p = self.perimeter() / 2.0
            total = p
            for side in self.sides:
                total *= abs(p-side)
            return pow(total, 0.5)
    
        def is_valid(self):
            """ 判斷是否合法 """
            # 兩邊之和大於第三邊
            perimeter = self.perimeter()
            for side in self.sides:
                sum_two = perimeter - side
                if sum_two <= side:
                    raise InvalidePolygonError(str(self.__class__) + "is invalid")
            return True
    
    >>> t1 = Triangle(20, 20, 20)
    >>> t1.is_valid()
    True
    >>> t1.is_equilateral() 
    True
    >>> t1.area()
    173.20508075688772
    
    >>> # 建立一個不合法的三角形 
    ... 
    >>> t2 = Triangle(10, 20, 30) 
    >>> t2.is_valid()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 28, in is_valid
    __main__.InvalidePolygonError: <class '__main__.Triangle'>is invalid
    

    使用介面卡模式實現矩形類,預期介面為is_square

    class Rectangle(Polygon):
        """ 使用介面卡模式實現矩形 """
        def is_square(self):
            """ 判斷是否為正方形 """
            if self.is_valid():
                return self.is_regular()
        
        def is_valid(self):
            """ 判斷是否合法 """
            # 4條邊
            if len(self.sides) != 4:
                return False
            # 對邊相等
            for a,b in [(0,2), (1,3)]:
                if self.sides[a] != self.sides[b]:
                    return False
            return True
    
        def area(self):
            """ 計算面積 """
            if self.is_valid():
                return self.sides[0]*self.sides[1]
    
    >>> r1 = Rectangle(10, 20, 10, 20) 
    >>> r1.is_valid()
    True
    >>> r1.is_square()
    False
    >>> r1.area()
    200
    >>> # 建立一個正方形
    ... r2 = Rectangle(10, 10, 10, 10) 
    >>> r2.is_square()
    True
    
  • 物件介面卡的方式

    class Triangle2:
        """ 物件介面卡方式實現三角形類 """
    
        def __init__(self, *sides):
            """ 例項化基類物件 """
            self.polygon = Polygon
            
        def perimeter(self):
            return self.polygon.perimeter
        
        def is_valid(f):
            """ 判斷是否為三角形 """
            def inner(self, *args):
                perimeter = self.polygon.perimeter()
                sides = self.polygon.sides
    
                for side in sides:
                    sum_two <= side:
                        raise InvalidePolygonError(str(self.__class__) +
                                                    "is invalid"
                                                    )
                result = f(self, *args)
                return result
            return inner
    
        @is_valid
        def is_equilateral(self):
            """ 判斷是否為等邊三角形 """
            return self.polygon.is_regular()
    
        @is_valid
        def is_isosceles(self):
            """ 判斷是否為等腰三角形 """
            for a,b in itertools.combinations(self.polygon.sides, 2):
                if a == b:
                    return True
            return False
    
        def area(self):
            """ 計算面積 """
            p = self.polygon.perimeter() / 2.0
            total = p
            for side in self.polygon.sides:
                total *= abs(p-side)
            return pow(total, 0.5)
    
    • 物件介面卡方式與類介面卡方式的主要區別:

      • 物件介面卡無需繼承想要適配的類,需要例項化適配的類。
      • 物件介面卡類中用到的適配的類中的屬性和方法都必須顯示的通過例項化物件獲取。
    • 升級版物件適配方式:重寫___getatttr__方法動態獲取需要適配的類中的屬性。

      class Triangle2:
          """ 物件介面卡方式實現三角形類 """
          
          # 如果想要呼叫的方法與例項物件上的方法內容一樣,但是方法名不一樣,
          # 可以通過對映的方式來定義兩個方法名間的關係
          method_mapper = {'is_equilateral':'is_regular'}
      
          def __init__(self, *sides):
              """ 例項化基類物件 """
              self.polygon = Polygon(*sides)
          
          def is_valid(f):
              """ 判斷是否為三角形 """
              def inner(self, *args):
                  perimeter = self.perimeter()
                  sides = self.sides
      
                  for side in sides:
                      sum_two = perimeter - side
                      if sum_two <= side:
                          raise InvalidePolygonError(str(self.__class__) + "is invalid")
                  result = f(self, *args)
                  return result
              return inner
      
          @is_valid
          def is_isosceles(self):
              """ 判斷是否為等腰三角形 """
              for a,b in itertools.combinations(self.sides, 2):
                  if a == b:
                      return True
              return False
      
          @is_valid
          def area(self):
              """ 計算面積 """
              p = self.perimeter() / 2.0
              total = p
              for side in self.sides:
                  total *= abs(p-side)
              return pow(total, 0.5)
      
          def ___getatttr__(self, name):
              """
              首先查詢在對映字典中是否存在對應的方法名,
              如果存在就拿對應的例項物件的方法名去獲取其屬性。
              否則就直接從例項物件中查詢屬性。
              """
              name = self.method_mapper.get(name, name)
              return getattr(self.polygon, name)
      
      >>> t1 = Triangle(2, 2, 2)
      >>> t1.is_equilateral()
      True
      >>> t1.area()
      1.7320508075688772
      >>>
      

外觀模式

需求

  • 當系統比較複雜時,無需關心繫統內部實現,只需要將對應操作的介面暴露出來即可。

實現

  • 定義汽車系統的各個類,然後使用外觀模式去提供啟動和停止介面。

    汽車系統類:

    class Engine:
        """ 汽車引擎類 """
    
        def __init__(self, name, bhp, rpm, volume, cylinders=4,
            type='petrol'):
            self.name = name
            self.bhp = bhp
            self.rpm = rpm
            self.volume = volume
            self.cylinders = cylinders
            self.type = type
            
        def start(self):
            """啟動引擎
            """
            print('Engine started')
    
        def stop(self):
            """停止引擎
            """
            print('Engine stopped')
    
    class Transmission:
        """變速類
        """
        def __init__(self, gears, torque):
            self.gears = gears
            self.torque = torque
            self.gears_pos = 0
    
        def shift_up(self):
            """ 加速 """
            if self.gears_pos == self.gears:
                print('不能加速了')
            else:
                self.gears_pos += 1
                print('加速至', self.gears_pos)
            
        def shift_down(self):
            """ 減速 """
            if self.gears_pos == -1:
                print('倒擋,不能減速了!')
            else:
                self.gears_pos -= 1
                print('減速至', self.gears_pos)
                
        def shift_reverse(self):
            """ 倒擋 """
            self.gears_pos = -1
            print('倒擋!')
            
        def shift_to(self, gear):
            """ 跳檔 """
            self.gears_pos = gear
            print(f'調製{gear}檔')
        
    class Brake:
        """ 剎車類 """
        
        def __init__(self, number, type='disc'):
            self.type = type
            self.number = number
            
        def engage(self):
            """ 進行剎車 """
            print('%s %d engage' % (self.__class__.__name__, self.number))
    
        def release(self):
            """ 恢復 """
            print('%s %d released' % (self.__class__.__name__, self.number))
    
    class ParkingBrake(Brake):
        """ 停車 """
        def __init__(self, type='drum'):
            super(ParkingBrake, self).__init__(type=type, number=1)
        
    class Suspension:
        """ 懸停 """
        def __init__(self, load, type='mcpherson'):
            self.type = type
            self.load = load
            
    class Wheel:
        """ 輪胎材料 """
        def __init__(self, material, diameter, pitch):
            self.material = material
            self.diameter = diameter
            self.pitch = pitch
            
    class WheelAssembly:
        """ 車輪 """
        def __init__(self, brake, suspension):
            self.brake = brake
            self.suspension = suspension
            self.wheels = Wheel('alloy', 'M12', 1.25)
    
        def aaply_brakes(self):
            """ 踩剎車 """
            print('踩剎車')
            self.brake.engage()
            
    class Frame:
        """ 外殼 """
        def __init__(self, length, width):
            self.length = length
            self.width = width
    

    組裝汽車類,並提供啟動和停止的介面:

    class Car:
        """ 汽車類——外觀模式 """
        def __init__(self, model, manufacturer):
            self.engine = Engine('K-series',85,5000, 1.3)
            self.frame = Frame(385, 170)
            self.wheel_assembiles = []
            for i in range(4):
                self.wheel_assembiles.append(WheelAssembly(Brake(i+1), Suspension(1000)))
            self.transmission = Transmission(5, 115)
            self.model = model
            self.manufacturer = manufacturer
            self.park_brake = ParkingBrake()
            
            self.ignition = False
            
        def start(self):
            """ 啟動 """
            print('啟動汽車~')
            self.ignition = True
            self.park_brake.release()
            self.engine.start()
            self.transmission.shift_up()
            print('汽車啟動了。。')
            
        def stop(self):
            """ 停止汽車 """
            print('停車~')
            for wheel_a in self.wheel_assembiles:
                wheel_a.apply_brakes()
                
            # 調製2擋然後1擋
            self.transmission.shift_to(2)
            self.transmission.shift_to(1)
            self.engine.stop()
            self.transmission.shift_to(0)
            self.park_brake.engage()
            print('停車完畢..')
    

    執行:

    >>> car = Car('Swift', 'Suzuki')          
    >>> car.start()
    啟動汽車~
    ParkingBrake 1 released
    Engine started
    加速至 1
    汽車啟動了。。
    >>> car.stop()
    停車~
    踩剎車        
    Brake 1 engage
    踩剎車        
    Brake 2 engage
    踩剎車
    Brake 3 engage
    踩剎車
    Brake 4 engage
    調製2檔
    調製1檔
    Engine stopped
    調製0檔
    ParkingBrake 1 engage
    停車完畢..
    

代理模式

需求

  • 包裝另一個物件來控制對此物件的訪問

實現

  • 記數代理:記錄前邊工廠模式產生的例項物件數量。

  • 這裡重用了Employee類。

    from abc import ABCMeta, abstractclassmethod
    
    class Employee(metaclass=ABCMeta):
        """員工抽象基類"""
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
        @abstractclassmethod
        def get_role(self):
            pass
    
        def __str__(self):
            return f"{self.__class__.__name__} - {self.name}, {self.age} years old {self.gender}"
        
    
    class Engineer(Employee):
        """繼承抽象基類,重寫抽象方法: 定義員工"""
    
        def get_role(self):
            return "engineering"
    
    
    class Accountant(Employee):
        """繼承抽象基類,重寫抽象方法: 定義員工"""
    
        def get_role(self):
            return "accountant"
    
    
    class Admin(Employee):
        """繼承抽象基類,重寫抽象方法: 定義員工"""
    
        def get_role(self):
            return "administration"
    

    建立代理工廠類:這裡使用的是物件代理,過載___getatttr__方法重定向訪問權,獲得原例項物件中的屬性。

    class EmployeeProxy:
        """ 員工計數代理類 """
        count = 0
    
        def __new__(cls, *args):
            """ 重寫構造方法,記錄員工數量 """
            instance = object.__new__(cls)
            cls.incr_count()
            return instance
    
        def __init__(self, employee):
            self.employee = employee
    
        @classmethod
        def incr_count(cls):
            """ 增加員工數量 """
            cls.count += 1
    
        @classmethod
        def decr_count(cls):
            """ 減少員工數量 """
            cls.count -= 1
        
        @classmethod
        def get_count(cls):
            """ 得到員工數量"""
            return cls.count
    
        def __str__(self):
            return str(self.employee)
    
        def __getattr__(self, name):
            """ 重定向獲取員工例項物件屬性 """
            return getattr(self.employee, name)
    
        def __del__(self):
            self.decr_count()
    
    
    
    class EmployeeProxyFactory:
        """ 返回計數代理物件的員工工廠類 """
    
        @classmethod
        def create(cls, name, *args):
            """ 生產員工的工廠類 """
            name = name.lower().strip()
    
            if name == 'engineer':
                return EmployeeProxy(Engineer(*args))
            elif name == 'accountant':
                return EmployeeProxy(Accountant(*args))
            elif name == 'admin':
                return EmployeeProxy(Admin(*args))
    
    >>> factory= EmployeeProxyFactory()  
    >>> engineer = factory. create ('engineer','Sam', 25,'M' )
    >>> EmployeeProxy.get_count()  
    1
    >>> admin = factory. create ('admin','Sam', 25,'M' )       
    >>> EmployeeProxy.get_count()
    2
    >>> accountant = factory. create ('accountant','Sam', 25,'M' ) 
    >>> EmployeeProxy.get_count()
    3
    >>> del admin
    >>> EmployeeProxy.get_count()
    2
    >>> del accountant
    >>> EmployeeProxy.get_count()
    1
    
  • Python的弱引用模組提供了實現代理物件的方法:

    >>> import weakref
    >>> import gc
    >>> engineer=Engineer('zzy', 25, 'M') 
    >>> # 檢視物件的引用計數
    ... len(gc.get_referrers(engineer))  
    1
    >>> # 現在建立一個對它的弱引用
    ... engineer_proxy = weakref.proxy(engineer) 
    >>> # 若引用具有和代理物件一樣的屬性
    ... print(engineer_proxy) 
    Engineer - zzy, 25 years old M
    >>> engineer_proxy.get_role()       
    'engineering'
    >>> # 檢視代理物件的引用計數並沒有增加
    ... len(gc.get_referrers(engineer)) 
    1
    >>>
    

1.5 Python模式——行為模式

行為模式是系統生命週期最後階段的模式,封裝了物件之間的通訊和互動,該模式往往傾向於物件組合。

迭代器模式

需求

  • 提供一種連續訪問容器物件中的元素而不會暴露底層物件本身的方法。

實現

  • Python中的迭代器必須實現__iter__方法,並且該物件還要能響應返回迭代器例項的iter函式。

  • 實現一個質數迭代器:

    
    class Prime:
        """ 一個質數迭代器 """
    
        def __init__(self, initial, final=0):
            self.current = initial
            # 迭代終止位,如果不傳,則會無限迭代
            self.final = final
    
        def __iter__(self):
            return self
    
        def __next__(self):
            return self._compute()
    
        def _compute(self):
            """ 計算下一個質數 """
            num = self.current
    
            while True:
                is_prime = True
                for x in range(2, int(pow(self.current, 0.5)+1)):
                    if self.current %x == 0:
                        is_prime = False
                        break
    
                num = self.current
                self.current += 1
    
                if is_prime:
                    return num
    				
                if self.final > 0 and self.current > self.final:
                    raise StopIteration
    
    >>> p = Prime(2, 10)
    >>> for num in p:
    ...     print(num) 
    ... 
    2
    3
    5
    7
    

    不傳入標誌位,結合itertools模組的方法可以達到多種結果:

    • 計算前100個質數:

      >>> p = Prime(2)
      >>> import itertools
      >>> list(itertools.islice(Prime(2), 100)) 
      [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 
      307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]
      
    • 過濾出個位以1結尾的前10個質數:

      >>> list(itertools.islice(itertools.filterfalse(lambda x: x % 10 != 1, Prime(2)), 10)) 
      [11, 31, 41, 61, 71, 101, 131, 151, 181, 191]
      

觀察者模式

需求

  • 解耦物件,允許在同一時刻一組物件(訂閱者)跟蹤另一組物件(釋出者)的改變。避免一對多的依賴和引用,同時保持它們的互動活動。
  • 該模式又叫做釋出者—訂閱模式。

實現

  • 實現一個Alam類,該類在自己的執行緒中執行,並且每隔1s生成定期報警。它也可以作為釋出者類,當發生報警時通知其訂閱者。

    import threading
    import time
    import pdb
    
    from datetime import datetime
    
    class Alarm(threading.Thread):
        """ 釋出者:定期通知類 """
    
        def __init__(self, duration=1):
            self.duration = duration
            self.subscribers = []
            self.flag = True
            threading.Thread.__init__(self)
    
        def register(self, subscriber):
            """ 為警告通知註冊一個訂閱者 """
            self.subscribers.append(subscriber)
    
        def notify(self):
            """ 通知所有訂閱者 """
            for subscriber in self.subscribers:
                subscriber.update(self.duration)
    
        def stop(self):
            """ 停止執行緒 """
            print('執行緒終止')
            self.flag = False
    
        def run(self):
            """ 執行警告通知 """
            while self.flag:
                time.sleep(self.duration)
                self.notify()
    
    
    class DumbClock:
        """ 訂閱者:訂閱Alarm物件並接收通知,同時利用通知更新它的時間 """
    
        def __init__(self):
            self.current = time.time()
        
        def update(self, *args):
            """ 釋出者回撥方法 """
            self.current += args[0]
    
        def __str__(self):
            return datetime.fromtimestamp(self.current).strftime('%H:%M:%S')
    
    
    if __name__ == "__main__":
        alarm = Alarm(duration=1)
        clock = DumbClock()
        alarm.register(clock)
        alarm.start()
        print(clock)
        time.sleep(5)
        print(clock)
    
    PS C:\Users\ZZY> & python c:/Users/ZZY/Desktop/a.py
    11:46:39
    11:46:43
    
    • 該模式的核心是要在釋出者類中要有註冊訂閱者的方法,也就是獲取訂閱者物件的方法,還有一個通知訂閱者的方法(該方法呼叫訂閱者中的接受更新的方法),所以在訂閱者中要有一個供釋出者呼叫的介面用來接受釋出者的訊息。

狀態模式

需求

  • 將物件的內部狀態封裝到另一個類(狀態物件)。物件通過將內部封裝的狀態切換到不同的值來更改其自身狀態。

實現

  • Python中的物件可以通過__class__類屬性獲取類中的屬性和方法。我們可以通過修改__class__屬性達到切換狀態的效果。

  • 實現一個可以切換電腦執行狀態的案例,該案例使用迭代器的方式去切換狀態:

    import random
    
    
    class ComputerState:
        """ 電腦狀態基類 """
    
        name = "state"
        next_states = []
        random_states = []
    
        def __init__(self):
            self.index = 0
        
        def __str__(self):
            return self.__class__.__name__
    
        def __iter__(self):
            return self
    
        def change(self):
            return self.__next__()
    
        def set(self, state):
            """
            設定狀態:
            如果該狀態存在於下個狀態列表中,首先會切換至下個狀態,
            如果不存在下個狀態列表中,就切換至隨機狀態列表的狀態
            """
            if self.index < len(self.next_states):
                if state in self.next_states:
                    self.index = self.next_states.index(state)
                    self.__class__ = eval(state)
                    return self.__class__
                else:
                    current = self.__class__
                    new = eval(state)
                    raise Exception(f'Illegal transition from {current} to {new}')
            else:
                self.index = 0
                if state in self.random_states:
                    self.__class__ = eval(state)
                    return self.__class__
    
        def __next__(self):
            """ 切換至下個狀態:
                如果存在下個狀態,首先會切換至下個狀態,
                如果不存在下個狀態,就切換至隨機狀態
            """
            if self.index < len(self.next_states):
                self.__class__ = eval(self.next_states[self.index])
                self.index += 1
                return self.__class__
            else:
                self.index = 0
                if len(self.random_states):
                    state = random.choice(self.random_states)
                    self.__class__ = eval(state)
                    return self.__class__
                else:
                    raise StopIteration
    
    
    class ComputerOff(ComputerState):
        """
        關機狀態:
        關機狀態固定下一個狀態為開機狀態,
        隨機狀態有掛起,休眠,關機
        """
        next_states = ['ComputerOn']
        random_states= ['ComputerSuspend', 'ComputerHibernate', 'ComputerOff']
    
    class ComputerOn(ComputerState):
        """
        開機狀態:
        沒有固定的下一個狀態,
        隨機狀態有掛起,休眠,關機
        """
        random_states = ['ComputerSuspend','ComputerHibernate','ComputerOff']  
    
    class ComputerWakeUp(ComputerState):
        """
        執行狀態:
        沒有固定的下一個狀態,
        隨機狀態有掛起、休眠、關機
        """
        random_states = ['ComputerSuspend','ComputerHibernate','ComputerOff']
    
    class ComputerSuspend(ComputerState):
        """
        掛起狀態:
        掛起狀態下一個狀態為執行狀態,
        隨機狀態有掛起、休眠、關機
        """
        next_states = ['ComputerWakeUp']
        random_states = ['ComputerSuspend','ComputerHibernate','ComputerOff']
    
    class ComputerHibernate(ComputerState):
        """
        休眠狀態:
        休眠狀態下一個固定狀態為開機狀態,
        隨機狀態有掛起、休眠、關機
        """
        next_states = ['ComputerOn']
        random_states = ['ComputerSuspend','ComputerHibernate','ComputerOff']
    
    
    class Computer:
        """ 可以改變狀態的電腦類 """
    
        def __init__(self, model):
            self.model = model
            # 預設為關機
            self.state = ComputerOff()
    
        def change(self, state=None):
            """ 修改至指定狀態 """
            if state == None:
                return self.state.change()
            else:
                return self.state.set(state)
    
        def __str__(self):
            """ 返回狀態 """
            return str(self.state)
            
    

    執行結果:

    >>> c = Computer('HUAWEI') 
    >>> c.change()				
    <class '__main__.ComputerOn'>
    >>> c.change('ComputerOff') 
    <class '__main__.ComputerOff'>
    >>> c.change()
    <class '__main__.ComputerOn'>
    >>> c.change()
    <class '__main__.ComputerSuspend'>
    >>> c.change()
    <class '__main__.ComputerWakeUp'>
    >>> c.change()
    <class '__main__.ComputerOff'>
    

總結

對Python學習瞭解越深,越能體會人生苦短,我用Python這短短几個字中的含義:

  • 優雅
  • 人性化思維

當我們學會以物件導向的思維去學習設計模式,會發現設計模式並沒有想象的複雜:不就是變著法去操作過來操作過去一些物件嘛。

個人覺得學習設計模式更重要的是理解它為什麼要這樣設計,以及它解決了什麼問題。

關於如何實現這點要結合具體的程式語言,但核心點就是物件:如何去獲取、改變、組裝物件的屬性。

相關文章