Python-Fliegengewicht-Muster

Das Flyweight-Muster ist ein strukturelles Entwurfsmuster, das den Speicherverbrauch minimiert und die Leistung durch gemeinsame Nutzung von Objekten verbessert. Einfach ausgedrückt ist es die Idee des „Teilens von Metadaten“.

Stellen Sie sich vor, Sie leihen sich Bücher aus einer Bibliothek: Wenn jedes Buch von verschiedenen Personen separat gekauft würde, müsste die Bibliothek Tausende identischer Bücher lagern. In Wirklichkeit muss die Bibliothek jedoch nur ein Exemplar von „Einführung in Python“ aufbewahren, und jeder, der es lesen möchte, kann dasselbe Exemplar ausleihen. Das Flyweight-Muster ist eine solche „Bibliothek“, die gemeinsam nutzbare Objekte verwaltet.

Kernidee

Das Flyweight-Muster teilt den Zustand von Objekten in zwei Arten auf:

  • Intrinsischer Zustand: der unveränderliche, gemeinsam nutzbare Teil
  • Extrinsischer Zustand: der veränderliche, nicht gemeinsam nutzbare Teil

Durch die gemeinsame Nutzung des intrinsischen Zustands wird vermieden, eine große Anzahl ähnlicher Objekte zu erzeugen, wodurch Systemressourcen gespart werden.


Warum brauchen wir das Flyweight-Muster

Problemszenario

Angenommen, wir entwickeln eine Textverarbeitung und müssen Zeichen in einem Dokument rendern. Wenn wir für jedes Zeichen ein separates Objekt erstellen:

Beispiel

# Schlechte Implementierung: jedes Zeichen ist ein unabhängiges Objekt
class Character:
    def __init__(self, char, font, size, color):
        self.char = char      # Zeichen
        self.font = font      # Schriftart
        self.size = size      # Schriftgröße
        self.color = color    # Farbe
   
    def render(self, position):
        print(f"Zeichen '{self.char}' an Position {position} rendern")

# Anwendungsbeispiel
char_a = Character('A', 'SimSun', 12, 'black')
char_b = Character('B', 'SimSun', 12, 'black')
char_a_another = Character('A', 'SimSun', 12, 'black')  # wiederholtes Erzeugen desselben 'A'

Probleme mit dieser Implementierung:

  • Speicherverschwendung: Dieselben Zeichen werden wiederholt erzeugt
  • Schlechte Leistung: Overhead durch das Erzeugen und Zerstören einer großen Anzahl von Objekten
  • Schwer zu warten: Die Anzahl der Objekte explodiert

Lösung

Mit dem Flyweight-Muster können wir:

  • Dieselben Zeichenobjekte gemeinsam nutzen
  • Nur eine Kopie des intrinsischen Zustands (das Zeichen selbst) speichern
  • Den extrinsischen Zustand (Position) beim Rendern übergeben

Implementierung des Flyweight-Musters

Grundstruktur

Implementieren wir das Flyweight-Muster anhand des Textverarbeitungsbeispiels:

Beispiel

from typing import Dict

# Flyweight-Klasse - speichert intrinsischen Zustand
class CharacterFlyweight:
    def __init__(self, char: str, font: str, size: int, color: str):
        self.char = char      # Intrinsischer Zustand: Zeicheninhalt
        self.font = font      # Intrinsischer Zustand: Schriftart
        self.size = size      # Intrinsischer Zustand: Schriftgröße
        self.color = color    # Intrinsischer Zustand: Farbe
   
    def render(self, position: tuple):
        """Rendere das Zeichen; Position ist der extrinsische Zustand"""
        x, y = position
        print(f"Rendere bei ({x}, {y}): Zeichen '{self.char}' "
              f"[Schriftart:{self.font}, Größe:{self.size}, Farbe:{self.color}]")

# Flyweight-Fabrik - verwaltet gemeinsam genutzte Objekte
class CharacterFactory:
    _characters: Dict[str, CharacterFlyweight] = {}
   
    @classmethod
    def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
        # Eindeutiger Identifikationsschlüssel zum Erstellen von Objekten
        key = f"{char}_{font}_{size}_{color}"
       
        # Wenn das Objekt nicht existiert, erstelle und speichere es im Cache
        if key not in cls._characters:
            cls._characters[key] = CharacterFlyweight(char, font, size, color)
            print(f"Erstelle neues Zeichenobjekt: {key}")
        else:
            print(f"Verwende vorhandenes Zeichenobjekt erneut: {key}")
       
        return cls._characters[key]

# Client-Klasse - verwendet Flyweight-Objekte
class TextDocument:
    def __init__(self):
        self.characters = []  # Speichert Zeichen- und Positionsinformationen
   
    def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
        # Holt Flyweight-Objekt aus der Factory
        character = CharacterFactory.get_character(char, font, size, color)
        # Speichert Zeichenobjekt und extrinsischen Zustand (Position)
        self.characters.append((character, position))
   
    def render(self):
        print("\n"=== Beginne mit dem Rendern des Dokuments ===")
        for character, position in self.characters:
            character.render(position)
        print("=== Rendern des Dokuments abgeschlossen ==="\n")

Anwendungsbeispiel

Beispiel

# Dokument erstellen
document = TextDocument()

# Zeichen zum Dokument hinzufügen
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0))  # 'l' wiederverwenden
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0))    # Andere Farbe, neues Objekt erstellen
document.add_character('H', 'Arial', 12, 'black', (0, 1))  # 'H' wiederverwenden

# Dokument rendern
document.render()

# Anzahl der Objekte in der Fabrik anzeigen
print(f"Die Fabrik hat insgesamt {len(CharacterFactory._characters)} Zeichenobjekte erstellt")

Ausgabeergebnis:

创建新字符对象: H_Arial_12_black
创建新字符对象: e_Arial_12_black
创建新字符对象: l_Arial_12_black
重用现有字符对象: l_Arial_12_black
创建新字符对象: o_Arial_12_black
创建新字符对象: !_Arial_12_red
重用现有字符对象: H_Arial_12_black

=== 开始渲染文档 ===
在位置(0, 0)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black]
在位置(1, 0)渲染: 字符'e' [字体:Arial, 大小:12, 颜色:black]
在位置(2, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black]
在位置(3, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black]
在位置(4, 0)渲染: 字符'o' [字体:Arial, 大小:12, 颜色:black]
在位置(5, 0)渲染: 字符'!' [字体:Arial, 大小:12, 颜色:red]
在位置(0, 1)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black]
=== 文档渲染完成 ===

工厂中总共创建了 6 个字符对象

Kernkomponenten des Flyweight-Musters

1. Flyweight (Flyweight-Schnittstelle oder abstrakte Klasse)

Definiert die Schnittstelle für Flyweight-Objekte, die üblicherweise Methoden für den Umgang mit extrinsischem Zustand enthält.

2. ConcreteFlyweight (Konkrete Flyweight-Klasse)

Implementiert die Flyweight-Schnittstelle und speichert den intrinsischen Zustand. Der intrinsische Zustand muss unveränderlich sein.

3. FlyweightFactory (Flyweight-Fabrik)

Erstellt und verwaltet Flyweight-Objekte und stellt sicher, dass Flyweight-Objekte korrekt gemeinsam genutzt werden.

4. Client

Verwaltet den extrinsischen Zustand und fordert bei Bedarf Flyweight-Objekte von der Flyweight-Fabrik an.


Ein komplexeres Beispiel: Anwendung in der Spieleentwicklung

Betrachten wir ein praktisches Beispiel aus der Spieleentwicklung – ein Baum-Rendering-System:

Beispiel

from typing import Dict, List
from dataclasses import dataclass

@dataclass
class TreeType:
    """Flyweight-Klasse – Baumtyp (intrinsischer Zustand)"""
    name: str          # Name der Baumart
    texture: str       # Texturdatei
    color: str         # Grundfarbe
   
    def render(self, x: int, y: int, height: int):
        """Rendert den Baum; Position und Höhe sind extrinsischer Zustand"""
        print(f"Rendere {self.name}-Baum bei ({x}, {y}), Höhe {height} Meter "
              f"[texture:{self.texture}, color:{self.color}]")

class TreeFactory:
    """Flyweight-Fabrik - verwaltet Baumtypen"""
    _tree_types: Dict[str, TreeType] = {}
   
    @classmethod
    def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
        key = f"{name}_{texture}_{color}"
        if key not in cls._tree_types:
            cls._tree_types[key] = TreeType(name, texture, color)
            print(f"Neuen Baumtyp erstellen: {name}")
        return cls._tree_types[key]
   
    @classmethod
    def list_tree_types(cls):
        print(f"\n"Es gibt derzeit {len(cls._tree_types)} Baumtypen:")
        for tree_type in cls._tree_types.values():
            print(f" - {tree_type.name}")

class Tree:
    """Tree-Objekt - enthält Flyweight-Referenz und extrinsischen Zustand"""
    def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
        self.x = x              # Extrinsischer Zustand: X-Koordinate
        self.y = y              # Extrinsischer Zustand: Y-Koordinate
        self.height = height    # Extrinsischer Zustand: height
        self.tree_type = tree_type  # Flyweight-Objekt-Referenz
   
    def render(self):
        self.tree_type.render(self.x, self.y, self.height)

class Forest:
    """Forest - Client-Klasse"""
    def __init__(self):
        self.trees: List[Tree] = []
   
    def plant_tree(self, x: int, y: int, height: int,
                   name: str, texture: str, color: str):
        tree_type = TreeFactory.get_tree_type(name, texture, color)
        tree = Tree(x, y, height, tree_type)
        self.trees.append(tree)
   
    def render(self):
        print("\n"=== Starte das Rendern des Waldes ===")
        for tree in self.trees:
            tree.render()
        print("=== Rendern des Waldes abgeschlossen ===")

# Verwendungsbeispiel
forest = Forest()

# Bäume pflanzen - Bäume desselben Typs teilen sich Flyweight-Objekte
forest.plant_tree(10, 20, 15, "Kiefer", "pine_texture.png", "Dunkelgrün")
forest.plant_tree(30, 40, 12, "Kiefer", "pine_texture.png", "Dunkelgrün")  # Kiefernbaumtyp wiederverwenden
forest.plant_tree(50, 60, 18, "Eiche", "oak_texture.png", "Hellgrün")
forest.plant_tree(70, 80, 20, "Kiefer", "pine_texture.png", "Dunkelgrün")  # Erneut wiederverwenden
forest.plant_tree(90, 100, 16, "Maple", "maple_texture.png", "Red")

# Wald rendern
forest.render()

# Baumtyp-Statistiken anzeigen
TreeFactory.list_tree_types()

Vor- und Nachteile des Flyweight-Musters

Vorteile

  1. Speicherverbrauch deutlich reduzieren: den Speicherverbrauch durch die gemeinsame Nutzung ähnlicher Objekte deutlich senken
  2. Leistung verbessern: den Overhead für Objekterstellung und Garbage Collection reduzieren
  3. Code-Wiederverwendung: dieselbe Objektlogik muss nur einmal implementiert werden
  4. Leicht erweiterbar: das Hinzufügen neuer Flyweight-Typen wirkt sich nicht auf vorhandenen Code aus

Nachteile

  1. Erhöht die Komplexität: es muss zwischen intrinsischem und extrinsischem Zustand unterschieden werden
  2. Thread-Sicherheitsprobleme: Gemeinsam genutzte Objekte erfordern eine zusätzliche Behandlung in Multithreading-Umgebungen
  3. Kann Fehler verursachen: Wenn der intrinsische Zustand versehentlich geändert wird, wirkt sich das auf alle Nutzer aus
  4. Nicht auf alle Szenarien anwendbar: Nur wirksam, wenn das Objekt wirklich gemeinsam nutzbar ist

Geeignete Szenarien

Szenarien, die für das Flyweight-Muster geeignet sind

  1. Große Anzahl ähnlicher Objekte: Wenn das System eine große Anzahl ähnlicher Objekte erstellen muss
  2. Speicherempfindliche Anwendungen: Speicherbeschränkte Umgebungen wie mobile Geräte und eingebettete Systeme
  3. Cache-Systeme: Wenn Objekte zwischengespeichert und wiederverwendet werden müssen
  4. Spieleentwicklung: Rendern einer großen Anzahl von Spielobjekten desselben Typs
  5. Dokumentverarbeitung: Textverarbeitungsprogramme, Tabellenkalkulationen usw.

Ungeeignete Anwendungsszenarien

  1. Objekte unterscheiden sich stark: Wenn jedes Objekt einen einzigartigen Zustand hat
  2. Komplexer externer Zustand: Wenn die Verwaltung des externen Zustands komplexer ist als die Objekterstellung
  3. Geringe Leistungsanforderungen: In Szenarien mit ausreichend Speicher und geringen Leistungsanforderungen

Übungsaufgaben

Übung 1: Verbessern Sie das Zeichen-Rendering-System

Versuchen Sie, unser vorheriges Zeichen-Rendering-System zu verbessern, indem Sie Unterstützung für Stile wie Fett und Kursiv hinzufügen:

Beispiel

# Ihr verbesserter Code gehört hierhin
class AdvancedCharacterFlyweight:
    # Unterstützung für Fett, Kursiv und Unterstreichung hinzufügen
    pass

# Testen Sie Ihre Implementierung
def test_advanced_system():
    # Erstellen Sie ein Dokument, das verschiedene Stile enthält
    pass

Übung 2: Implementieren Sie ein Icon-Management-System

Entwerfen Sie ein Icon-Management-System, bei dem dasselbe Icon dasselbe Objekt gemeinsam nutzt, wenn es an verschiedenen Positionen angezeigt wird:

Beispiel

class IconFlyweight:
    # Internen Zustand wie Icon-Dateipfad und Dimensionen speichern
    pass

class IconFactory:
    # Icon-Flyweight-Objekte verwalten
    pass

class Application:
    # Icons an verschiedenen Positionen auf der Oberfläche anzeigen
    pass

Zusammenfassung

Das Flyweight-Muster ist eine leistungsstarke Optimierungstechnik, die den Ressourcenverbrauch durch die gemeinsame Nutzung von Objekten reduziert. Wichtige Punkte:

  1. Zustände unterscheiden: Klar zwischen internem Zustand (teilbar) und externem Zustand (nicht teilbar) unterscheiden
  2. Eine Fabrik verwenden: Die Erstellung und gemeinsame Nutzung von Flyweight-Objekten über eine Fabrikklasse verwalten
  3. Vor- und Nachteile abwägen: Eine Balance zwischen Speichereinsparungen und Codekomplexität finden
  4. Anwendbare Szenarien: Hauptsächlich in Szenarien mit einer großen Anzahl ähnlicher Objekte verwendet
Weitere Erweiterungen