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
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
# 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
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 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
- Speicherverbrauch deutlich reduzieren: den Speicherverbrauch durch die gemeinsame Nutzung ähnlicher Objekte deutlich senken
- Leistung verbessern: den Overhead für Objekterstellung und Garbage Collection reduzieren
- Code-Wiederverwendung: dieselbe Objektlogik muss nur einmal implementiert werden
- Leicht erweiterbar: das Hinzufügen neuer Flyweight-Typen wirkt sich nicht auf vorhandenen Code aus
Nachteile
- Erhöht die Komplexität: es muss zwischen intrinsischem und extrinsischem Zustand unterschieden werden
- Thread-Sicherheitsprobleme: Gemeinsam genutzte Objekte erfordern eine zusätzliche Behandlung in Multithreading-Umgebungen
- Kann Fehler verursachen: Wenn der intrinsische Zustand versehentlich geändert wird, wirkt sich das auf alle Nutzer aus
- 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
- Große Anzahl ähnlicher Objekte: Wenn das System eine große Anzahl ähnlicher Objekte erstellen muss
- Speicherempfindliche Anwendungen: Speicherbeschränkte Umgebungen wie mobile Geräte und eingebettete Systeme
- Cache-Systeme: Wenn Objekte zwischengespeichert und wiederverwendet werden müssen
- Spieleentwicklung: Rendern einer großen Anzahl von Spielobjekten desselben Typs
- Dokumentverarbeitung: Textverarbeitungsprogramme, Tabellenkalkulationen usw.
Ungeeignete Anwendungsszenarien
- Objekte unterscheiden sich stark: Wenn jedes Objekt einen einzigartigen Zustand hat
- Komplexer externer Zustand: Wenn die Verwaltung des externen Zustands komplexer ist als die Objekterstellung
- 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
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
# 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:
- Zustände unterscheiden: Klar zwischen internem Zustand (teilbar) und externem Zustand (nicht teilbar) unterscheiden
- Eine Fabrik verwenden: Die Erstellung und gemeinsame Nutzung von Flyweight-Objekten über eine Fabrikklasse verwalten
- Vor- und Nachteile abwägen: Eine Balance zwischen Speichereinsparungen und Codekomplexität finden
- Anwendbare Szenarien: Hauptsächlich in Szenarien mit einer großen Anzahl ähnlicher Objekte verwendet