Skip to content

Latest commit

 

History

History
235 lines (155 loc) · 11.1 KB

File metadata and controls

235 lines (155 loc) · 11.1 KB

Flyweight Pattern

Zurück


(Credits: Blog von Vishal Chovatiya)


Wesentliche Merkmale

Kategorie: Structural Pattern

Ziel / Absicht:

In einem Satz:

„Das Flyweight Pattern reduziert den Speicherbedarf, indem es gemeinsame, unveränderliche Objektzustände zentral verwaltet und von mehreren Objekten wiederverwenden lässt.”

Das Flyweight-Pattern gehört zur Kategorie der strukturellen Entwurfsmuster und kommt zum Einsatz, wenn eine Anwendung sehr viele gleichartige Objekte verwalten muss, was ohne Gegenmaßnahmen zu hohem Speicherverbrauch führen würde.

Die Kernidee besteht darin, den Zustand eines Objekts in zwei Teile aufzuspalten: den intrinsischen Zustand, der zwischen vielen Objekten identisch ist und deshalb geteilt werden kann, und den extrinsischen Zustand, der für jede Instanz individuell ist und von außen übergeben werden muss.

Die intrinsischen Anteile werden in kompakten, unveränderlichen (immutable) Flyweight-Objekten zusammengefasst, die zentral verwaltet und wiederverwendet werden, statt sie für jedes logische Objekt neu anzulegen.

Eine Factory bzw. ein Pool übernimmt dabei die Aufgabe, bereits existierende Flyweight-Objekte zurückzugeben oder bei Bedarf neue zu erzeugen, sodass niemals unnötige Duplikate entstehen.

Der extrinsische Zustand wird nicht im Flyweight gespeichert, sondern erst beim Aufruf der jeweiligen Operation als Parameter mitgegeben. Dadurch sinkt die Anzahl tatsächlich im Speicher gehaltener Objekte drastisch, auch wenn logisch betrachtet Tausende oder Millionen „Instanzen” existieren.

Ein klassisches Beispiel ist die Darstellung von Zeichen in einem Texteditor, bei dem sich viele Zeichen dieselbe Glyphen- bzw. Formatinformation teilen, während Position und Kontext extern verwaltet werden.

Der Vorteil liegt in der erheblichen Speicherersparnis bei großer Objektanzahl. Der Preis dafür ist eine größere Trennung zwischen gemeinsamem und individuellem Zustand sowie eine möglicherweise aufwendigere Verwaltung der Objekte.

Struktur (UML):

Das folgende UML-Diagramm beschreibt eine Implementierung des Flyweight Patterns. Es besteht im Wesentlichen aus vier Teilen:

  • Flyweight: Beschreibt eine Klasse Flyweight, die den Teil des Status des zu betrachtenden Objekts enthält, der von mehreren Objekten gemeinsam genutzt werden kann. Dieses Objekt kann mehrfach verwendet werden. Der im Flyweight Objekt gespeicherte Zustand wird als „intrinsisch” oder „repeating” bezeichnet. Der Zustand, der an die Methoden des Flyweight Objekt übergeben wird, wird als „extrinsisch” bezeichnet. Ein Flyweight-Objekt muss „gemeinsam nutzbar” (sharable) sein. Jeder Zustand, den dieses Objekt speichert, muss intrinsisch sein.
  • FlyweightBase: Abstraktion der Klasse Flyweight zu einem Schnittstellentyp.
  • UnsharedFlyweight: Nicht alle Flyweight Objekte können gemeinsam genutzt werden. Die UnsharedFlyweight-Klasse enthält den extrinsischen Status, der für alle UnsharedFlyweight-Objekte eindeutig (unique) ist. Wenn ein UnsharedFlyweight-Objekt mit einem Flyweight-Objekt gepaart wird, repräsentiert es den vollständigen Status des zu betrachtenden Objekts.
  • FlyweightFactory: Die FlyweightFactory (Fabrik) erstellt und verwaltet Flyweight Objekte. Darüber hinaus verwaltet die Fabrik einen Pool unterschiedlicher Flyweight Objekte. Die Fabrik gibt bei einer entsprechenden Anforderung ein Objekt aus dem Pool zurück, falls es bereits erstellt wurde, oder fügt dem Pool ein Neues hinzu und gibt dieses zurück, falls dies der Anforderung entspricht.

Abbildung 1: Schematische Darstellung des Flyweight Patterns.


Hinweise zu Modern C++

  • std::string_view für zeigerbasierte Parameter:
    Übergibt man const std::string& an getFlyweight und weitere Methoden. Wenn man Literale wie "SharedState1" übergibt, wird implizit ein temporärer std::string (C++ 17) auf dem Heap erzeugt. std::string_view vermeidet diese Allokationen komplett.

  • Heterogene Lookups in std::unordered_map (C++ 20):
    Standardmäßig sucht std::unordered_map<std::string, ...> bei find() mit einem std::string_view nach einer impliziten Konvertierung (Heap-Allokation). Mit std::hash<void> (transparentes Hashing) kann man direkt mit einem std::string_view in der Map suchen, ohne einen std::string zu kopieren.

  • Effizientes Einfügen mit try_emplace (C++ 17):
    In getFlyweight suchst du zuerst mit find() und greifst danach doppelt mit m_flyweights[sharedState] zu. Das durchsucht die Map mehrfach. Mit try_emplace geschieht die Suche, Entscheidung und das Einfügen in einem einzigen Schritt.

  • Speicher-Overhead durch std::shared_ptr minimieren:
    Ein std::shared_ptr-Objekt verwaltet einen Kontrollblock (ca. 16 Byte) und den Zeiger. Da die Factory die Lebensdauer der Flyweights besitzt, reicht es oft, die Objekte per std::unique_ptr in der Map zu speichern und dem Client nur eine Referenz (const Flyweight&) oder sogar einen Rohzeiger (const Flyweight*) zurückzugeben. Das spart Speicher und erhöht die Cache-Lokalität.


Conceptual Example:

Quellcode 1
Quellcode 2


Erstes „Real-World” Beispiel:

Wir betrachten als Anwendung eine Spiele-Applikation, in der viele Bäume zu dekorativen Zwecken platziert sind. Einen solchen Baum (Klasse Tree) gibt es stilistisch in den drei Varianten „Palme”, „Zypresse” oder „Gartenbaum” mit jeweils eine Höhe von 30 Pixel.

Ein Tree-Objekt hat die folgenden Attribute:

  • Style - Stil des Baums. In unserem Beispiel entweder „palm”, „cypress” oder „garden”.
  • Height - Die Höhe des Baums ist immer 30 Pixel.
  • Position - Die Koordinate (x,y) des Baums in der Spielebene.

Abbildung 2: Viele identische Tree-Objekte werden in einer Anwendung verwendet.

Nehmen wir nun an, wir wollen 12.000 Bäume (jeweils die Stile „palm” –, „cypress” – und „garden” – zu 4000 Stück) im Spiel platzieren und den Bedarf des Speicherverbrauchs ermitteln, der erforderlich ist, um diese Baumobjekte im Speicher abzulegen.

Attribut Größe Beschreibung
Style 40 Bytes std::string-Objekt, bei den drei Zeichenketten "palm", "cypress" und "garden" kommt SSO zum Einsatz, folglich 40 Bytes pro Objekt (Visual C++).
Height 4 Bytes 4 Bytes zum Speichern einer int-Zahl.
Position 8 Bytes 4 Bytes pro x- und y-Koordinate zum Speichern einer int-Zahl.
Total 52 Bytes

Tabelle 1: Speicherbedarf eines Tree-Objekts.

Wir benötigen insgesamt 52 Bytes, um ein Tree-Objekt im Speicher abzulegen. Also brauchen wir 624.000 Byte (624 KB) zum Speichern von 12.000 Baumobjekten im Speicher. Aber wenn wir genau hinsehen, speichern wir in allen Objekten denselben Stil und dieselbe Höhe.

Wir verschwenden insgesamt 480.000 Bytes (480 KB) für den Stil und 48.000 Bytes (48 KB) für die Höhe, um doppelte Werte zu speichern. Die einzigen Daten, die variieren, sind die Position der Baumobjekte.

Nun identifizieren wir die intrinsischen und extrinsischen Zustände in diesem Beispiel:

  • Der intrinsische Zustand bezieht sich auf den Zustand, der zum Flyweight-Objekt gehört. Der intrinsische Zustand ist unveränderlich, in unserem Beispiel sind dies Stil und Höhe eines Baums.
  • Der extrinsische Zustand variiert für jedes Objekt und wird außerhalb des Objekts gespeichert. Der extrinsische Zustand ist veränderlich, wie zum Beispiel die Position eines Baums.

Damit legen wir folgende Realisierung im Beispiel zu Grunde:

  • Flyweight-Klasse Tree mit den intrinsischen Zuständen Stil und Höhe.
  • Klasse TreePosition, um den extrinsischen Zustand Position eines Flyweight-Objekts zu speichern.
  • Klasse TreeFactory mit einer einzigen Methode getTree(const std::string& style), die das Tree-Objekt mit dem angegebenen Stil erstellt. Die TreeFactory-Klasse verwaltet intern einen Cache, um die erstellten Flyweight-Objekte zu speichern.
  • Kontextklasse Game, die die Flyweight-Objekte und ihren extrinsischen Zustand verwaltet.
  • Klasse Client, die für die Erstellung der Tree-Objekte verantwortlich ist, indem Sie die extrinsische Zustände (hier: Position) an das Game-Objekt übergibt.

Zweites „Real-World” Beispiel:

Wir betrachten als Beispiel eine Paint-Brush-Anwendung.

Der Anwender kann Pinsel-Objekte in drei Stärken benutzen: THICK, THIN and MEDIUM. Alle dicken (dünnen oder mittleren) Pinsel zeichnen den Inhalt auf genau dieselbe Weise – nur ist eben die Inhaltsfarbe anders.

Worauf kommt es an:

  • Die Pinselfarbe ist ein extrinsisches Attribut, das vom Aufrufer bereitgestellt wird, ansonsten bleibt für den Pinsel alles gleich.
  • Im Wesentlichen erstellen wir also nur dann einen Stift mit einer bestimmten Größe, wenn die Farbe anders ist. Sobald ein anderer Client diese Stiftgröße und -farbe benötigt, verwenden wir sie wieder.

Wir testen die Flyweight-Pen-Objekte in einem Beispielprogramm. Der Client erstellt hier zwei THICK-Stifte und einen THIN-Stift, aber zur Laufzeit sind es nur zwei Stiftobjekte, die in Aktion treten:

Ausgabe:

Drawing THICK content in color : YELLOW
Drawing THICK content in color : YELLOW
Drawing THICK content in color : BLUE

00000238DF950180      //same object
00000238DF950180      //same object
00000238DF950BA0

Quellcode


FAQs

Frage: Unterschied zwischen Singleton- und Flyweight Pattern?

Das Singleton-Pattern hilft uns, nur ein Objekt im System zu verwalten. Mit anderen Worten, sobald das erforderliche Objekt erstellt ist, können wir keine weiteren erstellen. Wir müssen das vorhandene Objekt in allen Teilen der Anwendung wiederverwenden.

Das Flyweight-Pattern wird verwendet, wenn wir eine große Anzahl ähnlicher Objekte erstellen müssen, die sich basierend auf dem vom Client bereitgestellten extrinsischen Attribut unterscheiden.


Literaturhinweise

Die Anregungen zum konzeptionellen Beispiel finden Sie unter

https://refactoring.guru/design-patterns

und

https://www.codeproject.com

vor.

Das PaintBrush-Beispiel ist an Flyweight Design Pattern angelehnt (Abruf: 15.07.2022).

Die Simulation eines Spiels mit vielen Bäumen wieder an Flyweight Design Pattern (Abruf: 21.01.2023).


Zurück