結構設計模式在軟體開發中扮演關鍵角色,有效解決物件間複雜互動。本文探討外觀模式和享元模式,並以 Python 程式碼示範其實際應用。外觀模式提供簡化的介面,讓客戶端無需理解子系統內部細節即可互動。享元模式則透過分享物件,減少記憶體消耗並提升效能,尤其適用於大量相似物件的場景。文章也比較了享元模式與 Memoization 的差異,並簡述了代理模式的相關概念,提供更全面的理解。

結構設計模式:外觀模式與輕量模式

在軟體開發中,結構設計模式扮演著至關重要的角色,用於解決物件之間的複雜互動問題。本文將探討兩種重要的結構設計模式:外觀模式(Facade Pattern)與輕量模式(Flyweight Pattern),並透過例項解析其應用與優勢。

外觀模式(Facade Pattern)

外觀模式是一種簡化複雜系統介面的設計模式,它提供了一個統一的介面,讓客戶端可以輕鬆地與子系統互動,而無需瞭解子系統的內部實作細節。

例項解析:作業系統外觀

考慮一個簡化的作業系統範例,其中包含多個伺服器,如檔案伺服器(FileServer)、處理程式伺服器(ProcessServer)等。客戶端程式需要與這些伺服器互動,但無需瞭解其內部工作機制。

class OperatingSystem:
    """外觀類別"""
    def __init__(self):
        self.fs = FileServer()
        self.ps = ProcessServer()

    def start(self):
        [i.boot() for i in (self.fs, self.ps)]

    def create_file(self, user, name, perms):
        return self.fs.create_file(user, name, perms)

    def create_process(self, user, name):
        return self.ps.create_process(user, name)

def main():
    os = OperatingSystem()
    os.start()
    os.create_file("foo", "hello.txt", "-rw-r-r")
    os.create_process("bar", "ls /tmp")

內容解密:

  • OperatingSystem 類別作為外觀,封裝了 FileServerProcessServer 的例項。
  • start 方法啟動所有伺服器。
  • create_filecreate_process 方法分別委託給對應的伺服器處理。
  • 客戶端程式只需與 OperatingSystem 互動,無需瞭解內部伺服器的細節。

輕量模式(Flyweight Pattern)

輕量模式旨在透過分享物件來減少記憶體使用和提高效能,特別適用於需要建立大量相似物件的場景。

例項解析:第一人稱射擊遊戲

在第一人稱射擊遊戲中,多個玩家角色可能分享某些狀態,如外觀和行為。透過輕量模式,可以將這些分享狀態抽取出來,形成一個輕量物件。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 例項解析:第一人稱射擊遊戲

rectangle "分享" as node1
rectangle "獨有" as node2

node1 --> node2

@enduml

圖表翻譯: 此圖示展示了角色物件如何分享輕量物件,同時擁有自己的外部狀態。

內容解密:

  • 角色物件包含分享的輕量物件和獨有的外部狀態。
  • 輕量物件包含不變的內在狀態,如外觀和行為。
  • 外部狀態是可變的,特定於每個角色物件。

享元模式(Flyweight Pattern)詳解

享元模式是一種用於最佳化效能和記憶體使用的設計模式,特別適用於需要大量物件的應用程式。透過分享物件資料,享元模式能夠減少記憶體分配並提升系統效率。

享元模式的應用場景

享元模式主要用於以下情況:

  • 當應用程式需要使用大量物件時。
  • 當物件數量龐大到儲存或渲染成本過高時。
  • 當物件的身份識別並不重要時,因為分享物件可能會導致身份比較失敗。

享元模式的實作

以下是一個使用享元模式的範例,模擬一個停車場中的汽車。這個範例展示瞭如何透過分享相同型別的汽車物件來節省記憶體。

首先,定義一個列舉型別 CarType,描述三種不同型別的汽車:

from enum import Enum
import random

CarType = Enum("CarType", "SUBCOMPACT COMPACT SUV")

接下來,定義核心類別 Carpool 變數作為物件池(快取)。使用 __new__ 方法將 Car 類別轉換為支援自我參照的元類別。當客戶端程式碼建立 Car 例項時,會根據汽車型別檢查是否已存在相同型別的汽車。如果存在,則傳回先前建立的物件;否則,將新的汽車型別新增到池中並傳回:

class Car:
    pool = dict()

    def __new__(cls, car_type):
        obj = cls.pool.get(car_type, None)
        if not obj:
            obj = object.__new__(cls)
            cls.pool[car_type] = obj
            obj.car_type = car_type
        return obj

    def render(self, color, x, y):
        type = self.car_type
        msg = f"render a {color} {type.name} car at ({x}, {y})"
        print(msg)

內容解密:

  1. Car 類別的 __new__ 方法:負責控制物件的建立過程。如果指定型別的汽車已經存在於 pool 中,則直接傳回現有的物件;否則,建立新物件並加入 pool
  2. render 方法:用於在螢幕上渲染汽車。客戶端程式碼需要提供顏色和座標等可變資訊。
  3. 分享物件:相同的 CarType 對應相同的 Car 物件例項,節省記憶體。

主函式範例

主函式展示瞭如何使用享元模式。汽車的顏色從預定義的顏色列表中隨機選擇,座標值也在指定範圍內隨機生成。儘管渲染了 18 輛汽車,但記憶體只為 3 種不同型別的汽車分配空間。輸出的最後一行證明瞭在使用享元模式時,不能依賴物件身份識別,因為相同型別的汽車物件具有相同的身份:

def main():
    rnd = random.Random()
    colors = ["white", "black", "silver", "gray", "red", "blue", "brown", "beige", "yellow", "green"]
    min_point, max_point = 0, 100
    car_counter = 0
    
    for _ in range(10):
        c1 = Car(CarType.SUBCOMPACT)
        c1.render(random.choice(colors), rnd.randint(min_point, max_point), rnd.randint(min_point, max_point))
        car_counter += 1
    
    for _ in range(3):
        c2 = Car(CarType.COMPACT)
        c2.render(random.choice(colors), rnd.randint(min_point, max_point), rnd.randint(min_point, max_point))
        car_counter += 1
    
    for _ in range(5):
        c3 = Car(CarType.SUV)
        c3.render(random.choice(colors), rnd.randint(min_point, max_point), rnd.randint(min_point, max_point))
        car_counter += 1

    # 驗證享元模式下的物件身份
    print(f"Total cars rendered: {car_counter}")
    c4 = Car(CarType.SUBCOMPACT)
    c5 = Car(CarType.SUBCOMPACT)
    print(f"Is c4 the same as c5? {c4 is c5}")  # 應該輸出 True,因為它們屬於同一享元家族

內容解密:

  1. main 函式:展示如何建立不同型別的汽車並渲染它們,同時統計渲染的汽車數量。
  2. 隨機顏色和座標:每次渲染汽車時,顏色和座標都是隨機生成的,模擬現實中的不同汽車例項。
  3. 物件身份比較:最後比較了兩個相同型別的汽車物件,證明它們是相同的例項。

Memoization 與享元模式的比較

Memoization 是一種最佳化技術,使用快取避免重複計算已有的結果,不限於特定的程式設計正規化。享元模式則是一種專注於分享物件資料的 OOP 特定最佳化設計模式。

結構設計模式中的享元模式與代理模式

在軟體設計中,結構設計模式關注於如何組織類別和物件以達成特定的功能,同時保持系統的彈性和可維護性。本篇文章將探討兩種重要的結構設計模式:享元模式(Flyweight Pattern)和代理模式(Proxy Pattern)。

享元模式

享元模式是一種用於最佳化記憶體使用和提高效能的設計模式。當系統中存在大量物件,且這些物件中有許多是重複或相似的,享元模式透過分享這些物件來減少記憶體的使用。

程式碼實作

以下是一個使用Python實作的享元模式範例,模擬了不同型別的汽車物件建立:

import random
from enum import Enum

class CarType(Enum):
    SUBCOMPACT = 1
    COMPACT = 2
    SUV = 3

class Car:
    pool = dict()

    def __new__(cls, car_type):
        obj = cls.pool.get(car_type, None)
        if not obj:
            obj = object.__new__(cls)
            cls.pool[car_type] = obj
            obj.car_type = car_type
        return obj

    def render(self, color, x, y):
        print(f"render a {color} {self.car_type.name} car at ({x}, {y})")

def main():
    car_counter = 0
    colors = ['red', 'black', 'white', 'gray', 'brown', 'beige', 'green']
    for _ in range(10):
        Car(CarType.SUBCOMPACT).render(random.choice(colors), random.randint(0, 100), random.randint(0, 100))
        car_counter += 1

    for _ in range(3):
        Car(CarType.COMPACT).render(random.choice(colors), random.randint(0, 100), random.randint(0, 100))
        car_counter += 1

    for _ in range(5):
        Car(CarType.SUV).render(random.choice(colors), random.randint(0, 100), random.randint(0, 100))
        car_counter += 1

    print(f"cars rendered: {car_counter}")
    print(f"cars actually created: {len(Car.pool)}")

    c4 = Car(CarType.SUBCOMPACT)
    c5 = Car(CarType.SUBCOMPACT)
    c6 = Car(CarType.SUV)
    print(f"{id(c4)} == {id(c5)}? {id(c4) == id(c5)}")
    print(f"{id(c5)} == {id(c6)}? {id(c5) == id(c6)}")

if __name__ == "__main__":
    main()

內容解密:

  1. Car類別使用__new__方法來控制物件的建立。如果指定型別的汽車物件已經存在,則傳回現有的物件;否則,建立一個新的物件並將其儲存在pool字典中。
  2. render方法用於模擬汽車的渲染,輸出汽車的顏色、型別和座標。
  3. main函式中,我們建立了不同型別的汽車物件並呼叫render方法,最後輸出渲染的汽車數量和實際建立的汽車物件數量。
  4. 透過比較不同汽車物件的id,驗證了享元模式的有效性。

代理模式

代理模式是一種透過引入代理物件來控制對真實物件的存取的設計模式。代理模式有多種型別,包括虛擬代理、保護代理、遠端代理和智慧代理。

程式碼實作

以下是一個簡單的虛擬代理範例,使用懶載入技術延遲昂貴物件的建立:

class ExpensiveObject:
    def __init__(self):
        # 模擬昂貴的初始化過程
        print("ExpensiveObject is being created...")

    def do_something(self):
        print("ExpensiveObject is doing something.")

class VirtualProxy:
    def __init__(self):
        self._expensive_object = None

    def do_something(self):
        if not self._expensive_object:
            self._expensive_object = ExpensiveObject()
        self._expensive_object.do_something()

def main():
    proxy = VirtualProxy()
    proxy.do_something()  # 第一次呼叫,建立ExpensiveObject
    proxy.do_something()  # 第二次呼叫,直接使用已建立的ExpensiveObject

if __name__ == "__main__":
    main()

內容解密:

  1. ExpensiveObject代表一個建立成本較高的物件。
  2. VirtualProxy類別作為代理,控制對ExpensiveObject的存取。只有在第一次呼叫do_something方法時,才會建立ExpensiveObject例項。
  3. main函式中,透過VirtualProxy例項呼叫do_something方法,展示了虛擬代理如何延遲昂貴物件的建立。

代理模式(Proxy Pattern)在軟體設計中的應用

代理模式是一種結構設計模式,它允許我們建立一個代理物件來控制對另一個物件的存取。在本篇文章中,我們將探討代理模式的兩種常見應用:延遲初始化(Lazy Initialization)和保護代理(Protection Proxy)。

延遲初始化(Lazy Initialization)

延遲初始化是一種技術,允許我們延遲物件的初始化直到它真正被需要時才進行。這種技術可以提高系統的效能和效率。

LazyProperty 類別

首先,我們建立一個 LazyProperty 類別,它可以用作裝飾器。當它裝飾一個屬性時,LazyProperty 會延遲載入該屬性,而不是立即載入。

class LazyProperty:
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        setattr(obj, self.method_name, value)
        return value

Test 類別

接下來,我們定義一個 Test 類別,它使用 LazyProperty 來延遲初始化 _resource 屬性。

class Test:
    def __init__(self):
        self.x = "foo"
        self.y = "bar"
        self._resource = None

    @LazyProperty
    def resource(self):
        print("initializing self._resource...")
        print(f"... which is: {self._resource}")
        self._resource = tuple(range(5))
        return self._resource

main() 函式

main() 函式中,我們展示了延遲初始化的行為。

def main():
    t = Test()
    print(t.x)
    print(t.y)
    # do more work...
    print(t.resource)
    print(t.resource)

輸出結果如下:

foo
bar
initializing self._resource...
... which is: None
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)

從輸出結果中,我們可以看到 _resource 變數是在第一次存取 t.resource 時才被初始化的,而不是在建立 t 物件時。

保護代理(Protection Proxy)

保護代理是一種代理模式,它控制對另一個物件的存取,以確保只有授權的客戶端才能存取該物件。

SensitiveInfo 類別

首先,我們定義一個 SensitiveInfo 類別,它包含我們想要保護的資訊。

class SensitiveInfo:
    def __init__(self):
        self.users = ["nick", "tom", "ben", "mike"]

    def read(self):
        nb = len(self.users)
        print(f"There are {nb} users: {' '.join(self.users)}")

    def add(self, user):
        self.users.append(user)
        print(f"Added user {user}")

Info 類別

接下來,我們定義一個 Info 類別,它是 SensitiveInfo 的保護代理。

class Info:
    def __init__(self):
        self.protected = SensitiveInfo()
        self.secret = "0xdeadbeef"

    def read(self):
        self.protected.read()

    def add(self, user):
        sec = input("what is the secret? ")
        if sec == self.secret:
            self.protected.add(user)
        else:
            print("That's wrong!")

main() 函式

main() 函式中,我們展示瞭如何使用保護代理。

def main():
    info = Info()
    while True:
        print("1. read list |==| 2. add user |==| 3. quit")
        key = input("choose option: ")
        if key == "1":
            info.read()
        elif key == "2":
            name = input("choose username: ")
            info.add(name)
        elif key == "3":
            exit()
        else:
            print(f"unknown option: {key}")

透過這種方式,我們可以控制對 SensitiveInfo 物件的存取,確保只有知道密碼的客戶端才能新增新的使用者。