Das Dekorierer-Muster (Decorator) stammt nicht von mir. Ich habe es in dem DesignPattern-Buch der Gang of Four (GoF) kennengelernt und versuche es hier in meinen Worten in Anlehnung an Dein Bsp zu beschreiben:
Häufig sollen Klassen um gewisse Funktionen erweitert werden. Recht häufig wird zu diesem Zweck die Vererbung von Klassen eingesetzt: Methoden werden überschrieben, in ihrer Sichtbarkeit verändert oder neue Methoden und Eigenschaften hinzugefügt.
Im Bsp der Streams gibt es nun für die Eingabe zB einen
FileInputStream, mit dessen Hilfe Dateien eingelesen werden können.
Liest man aus ihm Byte für Byte, sollte eine Art Puffer implementiert werden, damit die Performance, gedrosselt durch den langsamen Zugriff auf den tertiären Datenspeicher, erträglich bleibt. Der unbedarfte
OOP-Entwickler, könnte nun auf die Idee kommen, eine neue Funktionalität für die Pufferung zu implementieren, die er vielleicht über eine neue Eigenschaft (BufSize) zugänglich macht.
Nach einiger Zeit stellt man fest, das man von Zeit zu Zeit mit komprimierten Dateien arbeitet. Der Stream konnte erweitert werden, allerdings nur unter Zuhilfenahme einer Bibliothek von dritten, die das Programm vergrößert und in vielen Fällen nicht erwünscht ist, also: Neue Klasse, sagt der unbedarfte
OOP-Entwickler. Man könnte nun die Klasse
FileInputStream ableiten und die neue Funktionalität in den Erben
FileInputDecompressStream implementieren, der den bisherigen Puffer und das Lesen aus Dateien erbt. Je nachdem, ob das Dekomprimieren gewünscht ist, wird die eine oder andere Klasse benutzt (siehe: Farbrik-Muster).
Die plötzliche Forderung, das auch mit Verschlüsselungen gearbeitet werden soll, ist weiter kein Problem, zwar binden wir hierzu erneut eine Bibliothek ein, was ebenfalls mitunter nicht gewollt ist, aber durch das Anlegen der weiteren Erben
FileInputDecryptStream (entschlüssendes gepuffertes Lesen aus Dateien) und
FileInputDecompressDecryptStream (entschlüsselndes gepuffertes Lesen aus Dateien, die komprimiert sind) bekommen wir auch das in den Griff.
Hier erkennt man vielleicht schon das Dilemma... Aber was passiert erst, wenn der Stream morgen zunächst Entschlüsselt und anschließend entpackt werden soll? Was, wenn die Quelle vom Dateizugriff in einen Netzwerkzugriff geändert werden soll? Was ist, wenn wir eine Protokollfunktion implementieren wollen? Und wie bekommen wir alle Bugs aus den immer komplizierter werdenden Klassen heraus?
Wollte man jede denkbare Kombination implementieren, würde die Anzahl der Klassen explodieren!
Eine gängige Lösung ist hier das Dekorierer-Muster (Quelle: GoF), das die Absicht hat, eine flexible Alternative zur Unterklassenbildung anzubieten, um Klassen zu erweitern.
Das Konzept stützt sich auf die in der
OOP gängigen Ideen des Geheimnisprinzips und der Zuständigkeit:
Es gibt eine abstrakte Oberklasse, den
InputStream, die lediglich abstrakte Methoden zum Lesen anbietet. Ein konkreter Erbe, zB der
FileInputStream implementiert diese Methoden so, dass aus Dateien, ein anderer, zB der
NetworkInputStream, realisiert sie, indem aus einer Netzwerkverbindung gelesen wird.
Die vorhin angesprochene Pufferung der Daten, ist für beide Streams interessant und ist im Wesentlichen nicht deren Aufgabe. Stattdessen implementieren wir einen
BufferedInputStream, der weder aus einer Datei noch von der Tastatur oder dem Netztwerk liest, sondern von einem anderen, beliebigen
InputStream (also zB ein
FileInputStream, aber auch ein anderer
BufferedInputStream selbst), auf den er eine Referenz inne hat. Er "umgibt" (wrapping) ihn sozusagen und "schmückt" oder "dekoriert" (decorates) ihn durch neue Funktionalität (hier: die Pufferung).
Und das ist im Wesentlichen schon das ganze Muster: Das äußere, ausschmückende Objekte heißt "Dekorierer" und hat dieselbe Schnittstelle wie das innere Objekt (weil sie eine gemeinsame Oberklasse haben und sich der Dekorierer ausschließlich auf die dort formulierte Schnittstelle beruft). Der Klient (der Programmierer) kann ihn also so benutzen, wie das dekorierte Objekt selbst (Transparenz).
Ein weiterer Dekorierer (
DecompressInputStream) könnte nun einen Stream so dekorieren, dass die enthaltenden Daten entpackt werden. Dabei ist es egal, als welchem konkreten
InputStream die Daten tatsächlich bezogen werden...
So ist eine beliebige Schachtelung möglich, während die Handhabung für den Entwickler vollständig transparent bleibt (eine Methode erwartet ein Exemplar von
InputStream, der konkrete Typ und die Tatsache, er ggf andere Streams dekoriert, ist irrelevant), die Zuständigkeiten geklärt sind (jede Klasse hat genau eine Aufgabe), und ohne, dass es zu eine Explosion von neuen Unterklassen kommt oder man bei der Entwicklung von mit immer komplexeren "Gott-Klassen" die Nerven verliert.
Puh! Geschafft
Ich hoffe, dass ich Dir mit dieser Erklärung helfen konnte.