在現代軟體開發中,系統架構的複雜度與資源管理的效率是兩個核心挑戰。Facade 設計模式提供了一種優雅的方式來隱藏系統內部的複雜性,讓客戶端能夠透過簡潔的介面與系統互動。Flyweight 設計模式則專注於記憶體使用的最佳化,透過物件分享機制來處理需要大量相似物件的場景。本文將深入探討這兩種結構型設計模式的核心原理、實作技巧與最佳實踐,並提供完整的 Python 程式碼範例來說明如何在實際專案中應用這些模式。

Facade 設計模式的核心概念

Facade 設計模式屬於結構型設計模式,其主要目的是為複雜的子系統提供一個統一且簡化的介面。在大型軟體系統中,通常會包含多個相互依賴的子系統,每個子系統都有自己的介面與實作細節。直接讓客戶端與這些子系統互動,不僅增加了耦合度,也使得客戶端程式碼變得複雜難以維護。Facade 模式透過引入一個高層介面,將這些複雜的互動封裝起來,讓客戶端只需要與 Facade 物件溝通。

這種設計帶來了多項優點。首先,它降低了客戶端與子系統之間的耦合度,客戶端不需要了解子系統的內部結構就能使用系統功能。其次,Facade 成為處理橫切關注點的理想位置,例如日誌記錄、錯誤處理、交易管理等功能都可以在 Facade 層集中實作。第三,當子系統的實作需要改變時,只需要修改 Facade 的內部實作,客戶端程式碼不需要任何調整。

在實務應用中,Facade 模式特別適合以下場景:當系統包含多個複雜的子系統且需要提供簡化的存取方式時;當需要將系統與客戶端解耦以便於獨立演進時;當需要在系統入口處統一處理驗證、授權、日誌等橫切關注點時。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

' Facade 模式結構
rectangle "客戶端" as client

rectangle "Facade" as facade {
  rectangle "統一介面" as interface
}

rectangle "子系統" as subsystem {
  rectangle "驗證服務" as auth
  rectangle "資料服務" as data
  rectangle "日誌服務" as log
  rectangle "快取服務" as cache
}

client --> facade : "簡化呼叫"
facade --> auth
facade --> data
facade --> log
facade --> cache

note bottom of facade
  封裝複雜的子系統互動
  提供簡潔的高層介面
  集中處理橫切關注點
end note

@enduml

實作整合多個子系統的 Facade

讓我們透過一個實際的範例來說明 Facade 模式的實作方式。假設我們正在開發一個需要整合身份驗證、資料擷取與日誌記錄的系統。這三個子系統各自擁有複雜的介面與實作,直接讓客戶端與它們互動會造成程式碼的複雜化。透過 Facade 模式,我們可以將這些互動封裝成單一的方法呼叫。

"""
Facade 設計模式實作範例
展示如何整合多個子系統並提供統一介面
"""

class AuthenticationService:
    """
    身份驗證服務子系統
    負責處理使用者身份驗證邏輯
    """

    def authenticate(self, username: str, password: str) -> str:
        """
        驗證使用者身份並回傳存取權杖

        參數:
            username: 使用者名稱
            password: 使用者密碼

        回傳:
            驗證成功時回傳存取權杖字串

        例外:
            ValueError: 當身份驗證失敗時拋出
        """
        # 實際應用中會包含更複雜的驗證邏輯
        # 例如密碼雜湊比對、帳戶狀態檢查等
        if username == "admin" and password == "secure_password":
            # 生成並回傳權杖
            # 實際應用會使用 JWT 或其他權杖機制
            return f"token_{username}_{hash(password)}"

        raise ValueError(f"使用者 '{username}' 身份驗證失敗")

class DataService:
    """
    資料服務子系統
    負責根據權杖擷取與處理資料
    """

    def __init__(self):
        # 模擬資料儲存
        self._data_store = {
            "users": [
                {"id": 1, "name": "張三", "role": "admin"},
                {"id": 2, "name": "李四", "role": "user"},
                {"id": 3, "name": "王五", "role": "user"}
            ],
            "products": [
                {"id": 101, "name": "筆記型電腦", "price": 35000},
                {"id": 102, "name": "無線滑鼠", "price": 800},
                {"id": 103, "name": "機械鍵盤", "price": 3500}
            ]
        }

    def fetch_data(self, token: str, data_type: str) -> dict:
        """
        根據權杖與資料類型擷取資料

        參數:
            token: 存取權杖
            data_type: 要擷取的資料類型

        回傳:
            包含請求資料的字典

        例外:
            ValueError: 當權杖無效或資料類型不存在時拋出
        """
        # 驗證權杖有效性
        if not token.startswith("token_"):
            raise ValueError("無效的存取權杖")

        # 檢查資料類型是否存在
        if data_type not in self._data_store:
            raise ValueError(f"資料類型 '{data_type}' 不存在")

        return {"type": data_type, "data": self._data_store[data_type]}

class LoggingService:
    """
    日誌服務子系統
    負責記錄系統事件與錯誤
    """

    def __init__(self):
        self._logs = []

    def log_event(self, level: str, message: str) -> None:
        """
        記錄事件到日誌系統

        參數:
            level: 日誌層級(INFO、WARNING、ERROR)
            message: 日誌訊息
        """
        import datetime
        timestamp = datetime.datetime.now().isoformat()
        log_entry = f"[{timestamp}] [{level}] {message}"
        self._logs.append(log_entry)
        print(log_entry)

    def get_logs(self) -> list:
        """取得所有日誌記錄"""
        return self._logs.copy()

class ServiceFacade:
    """
    服務外觀類別
    整合身份驗證、資料擷取與日誌記錄子系統
    提供簡化的統一介面給客戶端使用
    """

    def __init__(self):
        """初始化 Facade 並建立子系統實例"""
        self._auth_service = AuthenticationService()
        self._data_service = DataService()
        self._log_service = LoggingService()

    def get_user_data(self, username: str, password: str) -> dict:
        """
        取得使用者資料的簡化介面
        自動處理身份驗證、資料擷取與日誌記錄

        參數:
            username: 使用者名稱
            password: 使用者密碼

        回傳:
            包含處理後使用者資料的字典

        例外:
            Exception: 當任何步驟失敗時拋出
        """
        try:
            # 步驟 1:身份驗證
            self._log_service.log_event(
                "INFO",
                f"開始為使用者 '{username}' 進行身份驗證"
            )
            token = self._auth_service.authenticate(username, password)
            self._log_service.log_event(
                "INFO",
                f"使用者 '{username}' 身份驗證成功"
            )

            # 步驟 2:擷取資料
            self._log_service.log_event(
                "INFO",
                f"開始為使用者 '{username}' 擷取資料"
            )
            raw_data = self._data_service.fetch_data(token, "users")
            self._log_service.log_event(
                "INFO",
                f"成功擷取 {len(raw_data['data'])} 筆使用者資料"
            )

            # 步驟 3:處理資料
            processed_data = self._process_data(raw_data)
            self._log_service.log_event(
                "INFO",
                f"資料處理完成,共 {processed_data['count']} 筆記錄"
            )

            return processed_data

        except Exception as e:
            self._log_service.log_event(
                "ERROR",
                f"處理使用者 '{username}' 的請求時發生錯誤:{str(e)}"
            )
            raise

    def _process_data(self, raw_data: dict) -> dict:
        """
        內部方法:處理原始資料

        參數:
            raw_data: 原始資料字典

        回傳:
            處理後的資料字典
        """
        data_list = raw_data.get("data", [])

        return {
            "type": raw_data.get("type"),
            "count": len(data_list),
            "items": data_list,
            "summary": {
                "total_records": len(data_list),
                "data_type": raw_data.get("type")
            }
        }

# 使用範例
if __name__ == "__main__":
    # 建立 Facade 實例
    facade = ServiceFacade()

    # 透過簡化介面取得資料
    # 客戶端不需要知道內部有多個子系統
    try:
        result = facade.get_user_data("admin", "secure_password")
        print(f"\n處理結果:")
        print(f"  資料類型:{result['type']}")
        print(f"  記錄數量:{result['count']}")
        for item in result['items']:
            print(f"    - {item['name']} ({item['role']})")
    except Exception as e:
        print(f"發生錯誤:{e}")

這個範例展示了 Facade 模式如何將複雜的子系統互動封裝成單一的 get_user_data 方法。客戶端只需要提供使用者名稱與密碼,Facade 就會自動處理身份驗證、資料擷取、資料處理與日誌記錄等所有步驟。這種設計大幅簡化了客戶端程式碼,同時也讓系統的維護與擴展變得更加容易。

快取機制與效能最佳化

在實際應用中,Facade 經常需要處理資源密集型的操作,例如網路請求、資料庫查詢等。為了提升效能,我們可以在 Facade 中引入快取機制,避免重複執行相同的操作。

"""
帶有快取機制的 Facade 實作
展示如何透過快取提升效能
"""

import time
from typing import Optional, Any

class CachedServiceFacade(ServiceFacade):
    """
    具備快取功能的服務外觀
    繼承自 ServiceFacade 並加入快取機制
    """

    def __init__(self, cache_ttl: int = 300):
        """
        初始化快取 Facade

        參數:
            cache_ttl: 快取存活時間(秒),預設 300 秒
        """
        super().__init__()
        self._cache = {}  # 快取資料儲存
        self._cache_timestamps = {}  # 快取時間戳記
        self._cache_ttl = cache_ttl  # 快取存活時間

    def _get_from_cache(self, key: str) -> Optional[Any]:
        """
        從快取中取得資料

        參數:
            key: 快取鍵值

        回傳:
            快取的資料,若不存在或已過期則回傳 None
        """
        if key not in self._cache:
            return None

        # 檢查快取是否過期
        cached_time = self._cache_timestamps.get(key, 0)
        if time.time() - cached_time > self._cache_ttl:
            # 快取已過期,清除它
            del self._cache[key]
            del self._cache_timestamps[key]
            return None

        return self._cache[key]

    def _set_cache(self, key: str, value: Any) -> None:
        """
        將資料存入快取

        參數:
            key: 快取鍵值
            value: 要快取的資料
        """
        self._cache[key] = value
        self._cache_timestamps[key] = time.time()

    def get_user_data(self, username: str, password: str) -> dict:
        """
        取得使用者資料(帶快取功能)

        此方法會先檢查快取中是否有有效的權杖,
        若有則直接使用,否則才進行身份驗證

        參數:
            username: 使用者名稱
            password: 使用者密碼

        回傳:
            包含處理後使用者資料的字典
        """
        # 建立快取鍵值
        auth_cache_key = f"auth:{username}"
        data_cache_key = f"data:{username}:users"

        try:
            # 嘗試從快取取得權杖
            token = self._get_from_cache(auth_cache_key)

            if token:
                self._log_service.log_event(
                    "INFO",
                    f"使用快取的權杖為使用者 '{username}' 提供服務"
                )
            else:
                # 快取中沒有有效權杖,執行身份驗證
                self._log_service.log_event(
                    "INFO",
                    f"開始為使用者 '{username}' 進行身份驗證"
                )
                token = self._auth_service.authenticate(username, password)
                self._set_cache(auth_cache_key, token)
                self._log_service.log_event(
                    "INFO",
                    f"使用者 '{username}' 身份驗證成功,權杖已快取"
                )

            # 嘗試從快取取得資料
            cached_data = self._get_from_cache(data_cache_key)

            if cached_data:
                self._log_service.log_event(
                    "INFO",
                    f"為使用者 '{username}' 回傳快取的資料"
                )
                return cached_data

            # 快取中沒有資料,從資料服務擷取
            self._log_service.log_event(
                "INFO",
                f"開始為使用者 '{username}' 擷取資料"
            )
            raw_data = self._data_service.fetch_data(token, "users")
            processed_data = self._process_data(raw_data)

            # 將處理後的資料存入快取
            self._set_cache(data_cache_key, processed_data)
            self._log_service.log_event(
                "INFO",
                f"資料已擷取並快取,共 {processed_data['count']} 筆記錄"
            )

            return processed_data

        except Exception as e:
            self._log_service.log_event(
                "ERROR",
                f"處理使用者 '{username}' 的請求時發生錯誤:{str(e)}"
            )
            raise

    def clear_cache(self, username: Optional[str] = None) -> None:
        """
        清除快取

        參數:
            username: 若指定則只清除該使用者的快取,
                     否則清除所有快取
        """
        if username:
            # 清除特定使用者的快取
            keys_to_remove = [
                key for key in self._cache.keys()
                if username in key
            ]
            for key in keys_to_remove:
                del self._cache[key]
                del self._cache_timestamps[key]
            self._log_service.log_event(
                "INFO",
                f"已清除使用者 '{username}' 的快取"
            )
        else:
            # 清除所有快取
            self._cache.clear()
            self._cache_timestamps.clear()
            self._log_service.log_event(
                "INFO",
                "已清除所有快取"
            )

# 使用範例
if __name__ == "__main__":
    # 建立帶快取的 Facade
    cached_facade = CachedServiceFacade(cache_ttl=60)

    # 第一次呼叫會執行完整的驗證與資料擷取流程
    print("第一次呼叫:")
    result1 = cached_facade.get_user_data("admin", "secure_password")
    print(f"取得 {result1['count']} 筆記錄\n")

    # 第二次呼叫會使用快取
    print("第二次呼叫(使用快取):")
    result2 = cached_facade.get_user_data("admin", "secure_password")
    print(f"取得 {result2['count']} 筆記錄\n")

    # 清除快取後再次呼叫
    cached_facade.clear_cache("admin")
    print("清除快取後的呼叫:")
    result3 = cached_facade.get_user_data("admin", "secure_password")
    print(f"取得 {result3['count']} 筆記錄")

這個範例展示了如何在 Facade 中實作快取機制。透過快取權杖與資料,我們可以避免重複的身份驗證請求與資料擷取操作,大幅提升系統效能。快取具有過期時間設定,確保資料的新鮮度。同時也提供了清除快取的方法,讓系統能夠在需要時強制更新資料。

Flyweight 設計模式的核心概念

Flyweight 設計模式同樣屬於結構型設計模式,但它解決的問題與 Facade 截然不同。當系統需要建立大量相似的物件時,每個物件都佔用一定的記憶體空間,這可能導致嚴重的記憶體消耗問題。Flyweight 模式透過物件分享來解決這個問題,讓多個邏輯上獨立的物件能夠共用相同的底層資料。

Flyweight 模式的核心概念是將物件的狀態分為兩種:內在狀態與外在狀態。內在狀態是物件可以分享的部分,它是不可變的且不依賴於物件的使用情境。外在狀態則是每個物件獨有的部分,它依賴於使用情境且可能會變化。透過將內在狀態抽取出來並在多個物件間分享,Flyweight 模式能夠大幅降低記憶體使用量。

這種模式特別適合以下場景:當應用程式需要建立大量相似物件時;當物件的大部分狀態可以被抽取為外在狀態時;當移除外在狀態後,可以用少量的分享物件取代大量原本的物件時。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

' Flyweight 模式結構
rectangle "客戶端" as client

rectangle "Flyweight 工廠" as factory {
  rectangle "物件池" as pool
}

rectangle "Flyweight 物件" as flyweight {
  rectangle "內在狀態\n(分享)" as intrinsic
}

rectangle "外在狀態\n(獨立)" as extrinsic

client --> factory : "請求物件"
factory --> pool : "管理"
pool --> flyweight : "儲存"
client --> extrinsic : "維護"
client --> flyweight : "操作\n(傳入外在狀態)"

note bottom of flyweight
  多個客戶端分享
  相同的 Flyweight 物件
  只有內在狀態被儲存
end note

@enduml

實作字形渲染的 Flyweight 模式

讓我們透過一個字形渲染系統的範例來說明 Flyweight 模式的實作方式。在文字編輯器或排版系統中,需要渲染大量的字元。每個字元都有其字型、樣式等屬性,但許多字元會使用相同的字型與樣式組合。透過 Flyweight 模式,我們可以讓這些字元分享相同的字型與樣式物件,只在渲染時提供各自的位置與顏色資訊。

"""
Flyweight 設計模式實作範例
以字形渲染系統展示物件分享機制
"""

from typing import Tuple

class Glyph:
    """
    字形類別(Flyweight)
    封裝字元的內在狀態:字元本身、字型與樣式
    """

    def __init__(self, char: str, font: str, style: str):
        """
        初始化字形物件

        參數:
            char: 字元
            font: 字型名稱
            style: 樣式(如 Regular、Bold、Italic)
        """
        # 內在狀態:這些屬性在物件建立後不會改變
        # 且可以在多個使用情境間分享
        self._char = char
        self._font = font
        self._style = style

    @property
    def char(self) -> str:
        """取得字元"""
        return self._char

    @property
    def font(self) -> str:
        """取得字型"""
        return self._font

    @property
    def style(self) -> str:
        """取得樣式"""
        return self._style

    def render(self, x: int, y: int, color: str, size: int = 12) -> None:
        """
        渲染字形

        參數為外在狀態,由客戶端在呼叫時提供:
            x: X 座標位置
            y: Y 座標位置
            color: 顏色
            size: 字體大小
        """
        print(
            f"渲染 '{self._char}' "
            f"[{self._font}, {self._style}, {size}pt] "
            f"於 ({x}, {y}) "
            f"顏色:{color}"
        )

    def __repr__(self) -> str:
        return f"Glyph('{self._char}', {self._font}, {self._style})"

class GlyphFactory:
    """
    字形工廠類別
    負責管理 Glyph Flyweight 物件的建立與分享
    """

    # 類別層級的 Flyweight 物件池
    _flyweights = {}

    @classmethod
    def get_glyph(cls, char: str, font: str, style: str) -> Glyph:
        """
        取得字形物件

        若物件池中已存在相同內在狀態的物件則回傳它,
        否則建立新物件並加入物件池

        參數:
            char: 字元
            font: 字型名稱
            style: 樣式

        回傳:
            Glyph 物件
        """
        # 使用內在狀態作為快取鍵值
        key = (char, font, style)

        if key not in cls._flyweights:
            # 物件不存在,建立新的 Flyweight
            cls._flyweights[key] = Glyph(char, font, style)
            print(f"建立新的 Glyph:{key}")
        else:
            print(f"重複使用現有的 Glyph:{key}")

        return cls._flyweights[key]

    @classmethod
    def get_flyweight_count(cls) -> int:
        """取得物件池中的 Flyweight 數量"""
        return len(cls._flyweights)

    @classmethod
    def clear_pool(cls) -> None:
        """清除物件池"""
        cls._flyweights.clear()

    @classmethod
    def get_pool_info(cls) -> dict:
        """取得物件池的詳細資訊"""
        return {
            "total_flyweights": len(cls._flyweights),
            "flyweights": list(cls._flyweights.keys())
        }

class TextRenderer:
    """
    文字渲染器
    使用 Flyweight 模式來渲染文字
    """

    def __init__(self, default_font: str = "Arial",
                 default_style: str = "Regular"):
        """
        初始化文字渲染器

        參數:
            default_font: 預設字型
            default_style: 預設樣式
        """
        self._default_font = default_font
        self._default_style = default_style
        # 儲存要渲染的字形及其外在狀態
        self._render_queue = []

    def add_text(self, text: str, start_x: int, start_y: int,
                 color: str = "black", size: int = 12,
                 font: str = None, style: str = None) -> None:
        """
        將文字加入渲染佇列

        參數:
            text: 要渲染的文字
            start_x: 起始 X 座標
            start_y: 起始 Y 座標
            color: 文字顏色
            size: 字體大小
            font: 字型(若未指定則使用預設值)
            style: 樣式(若未指定則使用預設值)
        """
        font = font or self._default_font
        style = style or self._default_style

        x = start_x
        for char in text:
            if char == " ":
                # 空白字元不建立 Flyweight,只移動位置
                x += size // 2
                continue

            # 取得或建立 Flyweight
            glyph = GlyphFactory.get_glyph(char, font, style)

            # 儲存 Flyweight 與其外在狀態
            self._render_queue.append({
                "glyph": glyph,
                "x": x,
                "y": start_y,
                "color": color,
                "size": size
            })

            # 移動到下一個字元位置
            x += size

    def render_all(self) -> None:
        """渲染佇列中的所有字形"""
        print("\n開始渲染文字:")
        print("-" * 60)

        for item in self._render_queue:
            item["glyph"].render(
                item["x"],
                item["y"],
                item["color"],
                item["size"]
            )

        print("-" * 60)
        print(f"渲染完成,共 {len(self._render_queue)} 個字形")
        print(f"使用了 {GlyphFactory.get_flyweight_count()} 個 Flyweight 物件")

    def clear(self) -> None:
        """清除渲染佇列"""
        self._render_queue.clear()

# 使用範例
if __name__ == "__main__":
    # 清除之前的物件池
    GlyphFactory.clear_pool()

    # 建立文字渲染器
    renderer = TextRenderer(default_font="微軟正黑體", default_style="Regular")

    # 加入要渲染的文字
    print("準備渲染文字...\n")

    renderer.add_text(
        "HELLO WORLD",
        start_x=10,
        start_y=20,
        color="black",
        size=14
    )

    renderer.add_text(
        "HELLO FLYWEIGHT",
        start_x=10,
        start_y=40,
        color="blue",
        size=14
    )

    # 執行渲染
    renderer.render_all()

    # 顯示物件池資訊
    print("\n物件池資訊:")
    pool_info = GlyphFactory.get_pool_info()
    print(f"總共建立了 {pool_info['total_flyweights']} 個 Flyweight 物件")

    # 計算記憶體節省
    total_chars = len("HELLO WORLD") + len("HELLO FLYWEIGHT") - 2  # 扣除空白
    saved_objects = total_chars - pool_info['total_flyweights']
    print(f"總共需要渲染 {total_chars} 個字元")
    print(f"節省了 {saved_objects} 個物件的建立")

這個範例清楚展示了 Flyweight 模式的運作方式。當渲染相同的字元(例如 “HELLO” 中的 ‘L’)時,系統會重複使用同一個 Glyph 物件,而不是為每個字元建立新的物件。字元、字型與樣式作為內在狀態被分享,而位置、顏色與大小則作為外在狀態在渲染時提供。這種設計在需要渲染大量文字的應用中能夠顯著降低記憶體使用量。

UI 元件的 Flyweight 實作

Flyweight 模式在 UI 開發中也有廣泛的應用。當應用程式需要顯示大量具有相似樣式的元件時,例如列表中的項目或表格中的儲存格,可以讓這些元件分享相同的樣式物件。

"""
UI 元件的 Flyweight 模式實作
展示如何分享按鈕樣式以減少記憶體使用
"""

from typing import List, Tuple

class ButtonStyle:
    """
    按鈕樣式類別(Flyweight)
    封裝按鈕的視覺樣式屬性
    """

    def __init__(self, background_color: str, border_style: str,
                 border_radius: int, font_family: str):
        """
        初始化按鈕樣式

        所有參數都是內在狀態,在物件建立後不會改變

        參數:
            background_color: 背景顏色
            border_style: 邊框樣式
            border_radius: 圓角半徑
            font_family: 字型家族
        """
        self._background_color = background_color
        self._border_style = border_style
        self._border_radius = border_radius
        self._font_family = font_family

    @property
    def background_color(self) -> str:
        return self._background_color

    @property
    def border_style(self) -> str:
        return self._border_style

    @property
    def border_radius(self) -> int:
        return self._border_radius

    @property
    def font_family(self) -> str:
        return self._font_family

    def __repr__(self) -> str:
        return (
            f"ButtonStyle("
            f"bg={self._background_color}, "
            f"border={self._border_style}, "
            f"radius={self._border_radius}px, "
            f"font={self._font_family})"
        )

class ButtonStyleFactory:
    """
    按鈕樣式工廠
    管理 ButtonStyle Flyweight 物件
    """

    _styles = {}

    @classmethod
    def get_style(cls, background_color: str, border_style: str,
                  border_radius: int = 4,
                  font_family: str = "sans-serif") -> ButtonStyle:
        """
        取得按鈕樣式物件

        參數:
            background_color: 背景顏色
            border_style: 邊框樣式
            border_radius: 圓角半徑
            font_family: 字型家族

        回傳:
            ButtonStyle 物件
        """
        key = (background_color, border_style, border_radius, font_family)

        if key not in cls._styles:
            cls._styles[key] = ButtonStyle(
                background_color,
                border_style,
                border_radius,
                font_family
            )

        return cls._styles[key]

    @classmethod
    def get_style_count(cls) -> int:
        """取得樣式物件數量"""
        return len(cls._styles)

class Button:
    """
    按鈕元件類別
    使用 Flyweight 樣式物件
    """

    def __init__(self, label: str, style: ButtonStyle,
                 x: int, y: int, width: int, height: int):
        """
        初始化按鈕

        參數:
            label: 按鈕文字(外在狀態)
            style: 按鈕樣式(Flyweight,內在狀態)
            x: X 座標(外在狀態)
            y: Y 座標(外在狀態)
            width: 寬度(外在狀態)
            height: 高度(外在狀態)
        """
        self._label = label
        self._style = style  # 分享的 Flyweight 物件
        self._x = x
        self._y = y
        self._width = width
        self._height = height

    def render(self) -> None:
        """渲染按鈕"""
        print(
            f"渲染按鈕 '{self._label}' "
            f"於 ({self._x}, {self._y}) "
            f"大小:{self._width}x{self._height} "
            f"樣式:{self._style}"
        )

    def handle_click(self) -> None:
        """處理點擊事件"""
        print(f"按鈕 '{self._label}' 被點擊")

class ButtonPanel:
    """
    按鈕面板
    管理多個按鈕並展示 Flyweight 模式的效益
    """

    def __init__(self):
        self._buttons: List[Button] = []

    def add_button(self, label: str, x: int, y: int,
                   width: int = 100, height: int = 30,
                   style_type: str = "primary") -> Button:
        """
        加入按鈕到面板

        參數:
            label: 按鈕文字
            x: X 座標
            y: Y 座標
            width: 寬度
            height: 高度
            style_type: 樣式類型(primary/secondary/danger)

        回傳:
            建立的 Button 物件
        """
        # 根據樣式類型取得預定義的樣式
        style_configs = {
            "primary": ("#007bff", "solid", 4, "Arial"),
            "secondary": ("#6c757d", "solid", 4, "Arial"),
            "danger": ("#dc3545", "solid", 4, "Arial"),
            "success": ("#28a745", "solid", 4, "Arial"),
            "outline": ("#ffffff", "dashed", 4, "Arial"),
        }

        config = style_configs.get(style_type, style_configs["primary"])
        style = ButtonStyleFactory.get_style(*config)

        button = Button(label, style, x, y, width, height)
        self._buttons.append(button)
        return button

    def render_all(self) -> None:
        """渲染所有按鈕"""
        print("\n渲染按鈕面板:")
        print("-" * 80)

        for button in self._buttons:
            button.render()

        print("-" * 80)

    def get_statistics(self) -> dict:
        """取得統計資訊"""
        return {
            "total_buttons": len(self._buttons),
            "unique_styles": ButtonStyleFactory.get_style_count(),
            "memory_saved": len(self._buttons) - ButtonStyleFactory.get_style_count()
        }

# 使用範例
if __name__ == "__main__":
    # 建立按鈕面板
    panel = ButtonPanel()

    # 加入多個按鈕
    # 許多按鈕會分享相同的樣式
    panel.add_button("儲存", 10, 10, style_type="primary")
    panel.add_button("確認", 120, 10, style_type="primary")
    panel.add_button("提交", 230, 10, style_type="primary")

    panel.add_button("取消", 10, 50, style_type="secondary")
    panel.add_button("關閉", 120, 50, style_type="secondary")

    panel.add_button("刪除", 10, 90, style_type="danger")
    panel.add_button("移除", 120, 90, style_type="danger")
    panel.add_button("清除", 230, 90, style_type="danger")

    panel.add_button("新增", 10, 130, style_type="success")
    panel.add_button("建立", 120, 130, style_type="success")

    # 渲染所有按鈕
    panel.render_all()

    # 顯示統計資訊
    stats = panel.get_statistics()
    print(f"\n統計資訊:")
    print(f"  總按鈕數:{stats['total_buttons']}")
    print(f"  唯一樣式數:{stats['unique_styles']}")
    print(f"  節省的物件數:{stats['memory_saved']}")

    # 計算節省比例
    if stats['total_buttons'] > 0:
        save_ratio = (stats['memory_saved'] / stats['total_buttons']) * 100
        print(f"  記憶體節省比例:{save_ratio:.1f}%")

執行緒安全的 Flyweight 實作

在多執行緒環境中使用 Flyweight 模式時,需要特別注意執行緒安全問題。多個執行緒同時存取 Flyweight 工廠可能會導致競爭條件,造成重複建立物件或其他不一致的情況。以下範例展示如何實作執行緒安全的 Flyweight 工廠。

"""
執行緒安全的 Flyweight 工廠實作
確保在多執行緒環境下的正確性
"""

import threading
from typing import Dict, Tuple

class ThreadSafeGlyphFactory:
    """
    執行緒安全的字形工廠
    使用鎖機制確保多執行緒環境下的正確性
    """

    _flyweights: Dict[Tuple[str, str, str], 'Glyph'] = {}
    _lock = threading.Lock()

    @classmethod
    def get_glyph(cls, char: str, font: str, style: str) -> 'Glyph':
        """
        執行緒安全地取得字形物件

        參數:
            char: 字元
            font: 字型
            style: 樣式

        回傳:
            Glyph 物件
        """
        key = (char, font, style)

        # 使用雙重檢查鎖定模式(Double-Checked Locking)
        # 先不加鎖檢查,提升效能
        if key in cls._flyweights:
            return cls._flyweights[key]

        # 若不存在,則加鎖後再次檢查並建立
        with cls._lock:
            # 再次檢查,因為其他執行緒可能已經建立
            if key not in cls._flyweights:
                cls._flyweights[key] = Glyph(char, font, style)

            return cls._flyweights[key]

    @classmethod
    def get_flyweight_count(cls) -> int:
        """取得 Flyweight 數量"""
        with cls._lock:
            return len(cls._flyweights)

    @classmethod
    def clear_pool(cls) -> None:
        """清除物件池"""
        with cls._lock:
            cls._flyweights.clear()

def worker_function(thread_id: int, text: str) -> None:
    """
    工作執行緒函數
    模擬多個執行緒同時請求 Flyweight 物件
    """
    for char in text:
        glyph = ThreadSafeGlyphFactory.get_glyph(
            char, "Arial", "Regular"
        )
        # 模擬使用 Flyweight 進行某些操作
        print(f"執行緒 {thread_id}:處理字元 '{char}'")

# 使用範例
if __name__ == "__main__":
    # 清除物件池
    ThreadSafeGlyphFactory.clear_pool()

    # 建立多個執行緒
    threads = []
    texts = ["HELLO", "WORLD", "FLYWEIGHT"]

    for i, text in enumerate(texts):
        thread = threading.Thread(
            target=worker_function,
            args=(i, text)
        )
        threads.append(thread)

    # 啟動所有執行緒
    print("啟動執行緒...\n")
    for thread in threads:
        thread.start()

    # 等待所有執行緒完成
    for thread in threads:
        thread.join()

    # 顯示結果
    print(f"\n所有執行緒完成")
    print(f"物件池中共有 {ThreadSafeGlyphFactory.get_flyweight_count()} 個 Flyweight")

結合 Facade 與 Flyweight 模式

在實際應用中,不同的設計模式經常會結合使用。Facade 模式可以用來簡化對 Flyweight 工廠的存取,同時加入額外的功能如日誌記錄、效能監控等。

"""
結合 Facade 與 Flyweight 模式的範例
展示如何同時運用兩種模式
"""

class RenderingFacade:
    """
    渲染系統外觀
    整合 Flyweight 模式的字形工廠與其他服務
    """

    def __init__(self):
        self._render_count = 0
        self._cache_hits = 0
        self._cache_misses = 0

    def render_text(self, text: str, x: int, y: int,
                    font: str = "Arial", style: str = "Regular",
                    color: str = "black") -> None:
        """
        渲染文字的簡化介面

        參數:
            text: 要渲染的文字
            x: 起始 X 座標
            y: Y 座標
            font: 字型
            style: 樣式
            color: 顏色
        """
        current_x = x

        for char in text:
            if char == " ":
                current_x += 10
                continue

            # 檢查是否為快取命中
            key = (char, font, style)
            is_cached = key in GlyphFactory._flyweights

            # 取得 Flyweight
            glyph = GlyphFactory.get_glyph(char, font, style)

            # 更新統計
            if is_cached:
                self._cache_hits += 1
            else:
                self._cache_misses += 1

            # 渲染
            glyph.render(current_x, y, color)
            self._render_count += 1
            current_x += 12

    def get_statistics(self) -> dict:
        """取得渲染統計資訊"""
        total_requests = self._cache_hits + self._cache_misses
        hit_rate = (
            self._cache_hits / total_requests * 100
            if total_requests > 0 else 0
        )

        return {
            "total_renders": self._render_count,
            "cache_hits": self._cache_hits,
            "cache_misses": self._cache_misses,
            "hit_rate": f"{hit_rate:.1f}%",
            "flyweight_count": GlyphFactory.get_flyweight_count()
        }

# 使用範例
if __name__ == "__main__":
    GlyphFactory.clear_pool()

    # 使用 Facade 簡化渲染操作
    facade = RenderingFacade()

    print("使用 RenderingFacade 渲染文字:\n")

    facade.render_text("Hello World", 10, 20, color="blue")
    facade.render_text("Hello Again", 10, 40, color="red")

    # 顯示統計
    stats = facade.get_statistics()
    print(f"\n渲染統計:")
    print(f"  總渲染次數:{stats['total_renders']}")
    print(f"  快取命中:{stats['cache_hits']}")
    print(f"  快取未命中:{stats['cache_misses']}")
    print(f"  命中率:{stats['hit_rate']}")
    print(f"  Flyweight 數量:{stats['flyweight_count']}")

總結與最佳實踐

Facade 與 Flyweight 是兩種重要的結構型設計模式,各自解決不同的軟體設計問題。Facade 模式專注於簡化複雜系統的存取介面,而 Flyweight 模式則專注於最佳化記憶體使用。

在使用 Facade 模式時,應該注意幾個關鍵點。首先,Facade 應該只暴露客戶端真正需要的功能,避免成為一個臃腫的「上帝物件」。其次,Facade 不應該阻止客戶端直接存取子系統,當客戶端需要更細緻的控制時,應該允許它繞過 Facade。第三,可以考慮使用多個 Facade 來分離不同的關注點,例如一個處理使用者相關操作,另一個處理訂單相關操作。

在使用 Flyweight 模式時,關鍵在於正確識別內在狀態與外在狀態。內在狀態必須是不可變的,且能夠在多個物件間安全分享。外在狀態則由客戶端維護並在需要時傳入。此外,在多執行緒環境中,必須確保 Flyweight 工廠的執行緒安全性。

這兩種模式可以結合使用,Facade 可以簡化對 Flyweight 工廠的存取,同時提供額外的功能如統計、日誌、快取等。透過適當地運用這些設計模式,我們能夠建構出更加模組化、可維護且高效能的軟體系統。