Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Veröffentlicht: vor 2 monaten
Lesedauer ca. 15 Minuten

Das Prototype Design Pattern ist ein Erzeugungsmuster und ist somit auch für das Erstellen neuer Instanzen verantwortlich.

In dem Buch „GoF Buch“ wird es wie folgt beschrieben:

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Auf Deutsch soviel wie:

Beschreibt eine prototypische Instanz und erstellt neue Instanzen, indem der Prototyp kopiert wird.

Das Pattern hat also rein gar nichts mit dem gängigen Verständnis eines Prototypes zu tun. Stattdessen wird es verwendet, um vorhandene Objekte zu kopieren und zu klonen.

Der Aufbau des Prototype Pattern

Im UML Klassendiagramm, sieht der Prototype wie folgt aus:
Prototype pattern UML diagram

Wir haben also mindestens drei Klassen (oder eine Schnittstelle und eine Klasse), die wir mit Leben füllen müssen:

  • IPrototype definiert die Methode Clone() und lässt alle Objekte von sich Erben.
  • ConcretePrototype implementiert IPrototype und legt fest, wie es repliziert (bzw. kopiert) werden kann.
  • Der Client kann anschließend bei Bedarf beliebig viele Kopien erstellen und weiter verwenden.

Ein C# Beispielcode könnte etwa so aussehen:

interface IPrototype : ICloneable
{
	// new ist notwendig, um Clone in ICloneable zu überschreiben.
	// ICloneable ist standardmäßig in C# vorhanden.
	// Dieses Interface wird nur zur Veranschaulichung verwendet.
	new T Clone();
}

class ConcretePrototypeA : IPrototype
{
	public string Data { get; set; }
	// MemberwiseClone() kopiert bereits alle vorhandenen Werte,
	// Es ist jedoch eine protected Methode (am object)
	public ConcretePrototypeA Clone() => (ConcretePrototypeA) MemberwiseClone();
	object ICloneable.Clone() => Clone();
}

class ConcretePrototypeB : IPrototype
{
	public string Data { get; set; }
	public ConcretePrototypeA Reference { get; set; }
	public IList Iterable { get; set; }

	public ConcretePrototypeB Clone()
	{
		var clone = (ConcretePrototypeB)MemberwiseClone();
		// Referenztypen müssen ebenfalls händisch geklont werden, da hier
		// lediglich die Adresse kopiert wird. Beide Instanzen würden also 
		// auf dieselbe Instanz einer Klasse verweisen.
		clone.Reference = Reference.Clone();
		// Bei einer Collection, muss sichergestellt werden, dass neben einer 
		// neuen Collection, auch alle darin befindlichen Instanzen kopiert werden.
		clone.Iterable = Iterable.Select(i => i.Clone()).ToList();
		return clone;
	}

	object ICloneable.Clone() => Clone();
}

public class Client
{
	public void Main()
	{
		ConcretePrototypeA pa = new ConcretePrototypeA { Data = "A" };
		ConcretePrototypeA pa1 = new ConcretePrototypeA { Data = "A1" };
		ConcretePrototypeA pa2 = new ConcretePrototypeA { Data = "A2" };
		ConcretePrototypeB pb = new ConcretePrototypeB
		{
			Data = "B",
			Reference = pa,
			Iterable = new List {pa1, pa2}
		};

		ConcretePrototypeA ca = pa.Clone();
		ConcretePrototypeB cb = pb.Clone();

		ca.Data = "CA";
		cb.Reference.Data = "CB";
		cb.Iterable[0].Data = "CA1";

		Console.WriteLine($"pa := {pa.Data}; ca := {ca.Data}");
		Console.WriteLine($"pb := {pb.Reference.Data}; cb := {cb.Reference.Data}");
		Console.WriteLine($"pb := {pb.Iterable[0].Data}; cb := {cb.Iterable[0].Data}");

		/**
		 * OUTPUT:
		 * pa := A; ca := CA
		 * pb := A; cb := CB
		 * pb := A1; cb := CA1
		 */
	}
}

Der Unterschied zwischen einer tiefe und einer flachen Kopie

Grundsätzlich unterscheidet man zwischen zwei verschiedenen Arten einer Kopie.

Eine tiefe Kopie (deep copy) kopiert nicht nur das eigentliche Objekt, sondern auch die komplette Struktur.

Somit werden auch alle Collections und Objekte kopiert. Es werden also identische Objekte erstellt, die anschließend rein gar nichts miteinander zu tun haben. Man kann ein Objekt nach belieben verändern ohne das es Auswirkungen auf das andere Objekt haben könnte.

In dem gezeigten Beispiel wurde eine deep copy erstellt.

Eine flache Kopie (shallow copy) hingegen kopiert nur das Objekt selbst. Verwendete Referenzen bleiben jedoch erhalten.

Würde man im Original etwas an der Zweiten stelle etwas ändern, würde sich somit auch die Kopie verändern. In manchen Fällen ist das sogar sinnvoll.

Das erstellen einer flachen Kopie ist selbstverständlich viel performanter.

Aus diesem Grunde würde ich das dem Entwickler selber überlassen, welche Art er verwenden möchte und beides zulassen. Diese würde in etwa so aussehen:

interface IPrototype : ICloneable
{
	T DeepCopy();
	T ShallowCopy();

}
class ConcretePrototypeA : IPrototype
{
	public string Data { get; set; }
	public object Clone() => DeepCopy();
	public ConcretePrototypeA DeepCopy() => ShallowCopy();
	public ConcretePrototypeA ShallowCopy() => (ConcretePrototypeA)MemberwiseClone();
}

class ConcretePrototypeB : IPrototype
{
	public string Data { get; set; }
	public ConcretePrototypeA Reference { get; set; }
	public IList Iterable { get; set; }

	public object Clone() => DeepCopy();

	public ConcretePrototypeB DeepCopy()
	{
		var clone = ShallowCopy();
		clone.Reference = Reference.DeepCopy();
		clone.Iterable = Iterable.Select(o => o.DeepCopy()).ToList();
		return clone;
	}

	public ConcretePrototypeB ShallowCopy() => (ConcretePrototypeB)MemberwiseClone();
}

Gut, damit hätte man etwas Boilerplate-Code, aber was solls 🙂

Natürlich gibt es auch andere Möglichkeiten zum Erstellen einer Kopie. Diese können beispielsweise hier und hier angesehen werden.

Die klassische Weise wäre sowas wie:

public object Clone() 
{
	var clone = new Person();
	clone.ForeName = ForeName;
	clone.LastName = LastName;
}

Also alles schön per Hand kopieren 🙂

Beispiele aus dem Alltag

Bei einem Prototypen würde man spontan zunächst an die Industrie denken. Die Unternehmen erstellen neue Produkte und versuchen durch ein Prototypen bereits erste Erfahrungen zu sammeln.

Das hat jedoch herzlich wenig mit dem Prototype Design Pattern zu tun. Bei dieser Art von Prototypen wird nämlich keine Kopie vom Original erzeugt.

Den Durchbruch (wenn man das so nennen darf) beim Klonen hat man 1997 mit Dolly dem Schaf erreicht. Zum ersten mal hat man es geschafft eine lebende, exakte Kopie eines Organismus zu erschaffen.

Allerdings war Dolly nicht auf einmal als voll ausgewachsenes Schaf da, sondern musste genauso wie jedes andere Säugetier zunächst geboren werden.

Wir erleben tagtäglich Klone.

Beim Prozess der Zellteilung werden aus einer Zelle zwei komplett identische Zellen. Somit haben wir hier einen echten Klon – oder in unserem Kontext – einen Prototypen.

Die China Plagiate gehören übrigens nicht dazu. Die entstandenen Endprodukte sind meistens von minderer Qualität.

Beispiele aus der Praxis

Im Internet ist es in der Tat nicht einfach ein konkretes Beispiel zu finden. Sehr häufig sieht man an den Haaren herbeigezogene Beispiele. Ich möchte jedoch ein konkretes Beispiel aus der Praxis nennen.

Und in der Tat verwendet man das Prototype Pattern häufiger als man zunächst denken man.

Undo & Redo

Sobald man ein Undo haben möchte, muss man es schaffen den aktuellen Zustand zwischenzuspeichern und bei Bedarf an einer geeigneten Stelle abzurufen. Für das Memento Pattern ist ein Prototype unverzichtbar und kann auch nur mit diesem vernünftig abgebildet werden.

Immutable types

Wird ein immutable type – also ein schreibgeschütztes Objekt – benötigt, kommt ebenfalls das Prototype Pattern zum Einsatz. Was ein immutable type genau ist, kannst hier und hier im Detail nachlesen.

Serialisierung und Derialisierung

Jetzt denkst du bestimmt: „Spinnt der Sergej? Das ist doch keine exakte Kopie einer Instanz!“. Das mag vielleicht sogar stimmen, aber damit lässt sich trotzdem viel erreichen.

Kommunikation.

Durch Serialisierung kann beispielsweise eine Anfrage an eine andere Applikation gesendet und rekonstruiert werden.

Dies kann auch Zeitversetzt geschehen. Jedesmal, wenn ich eine Excel- oder Word Datei (in meinem Fall LibreOffice 🙂 ) abspeichere, erzeuge ich eine Kopie. Diese Kopie kann ich zu einem späteren Zeitpunkt wieder rekonstruieren und darauf weiterarbeiten.

Korrelation mit anderen Methodiken

Ich möchte gerne auf andere Modelle eingehen und schauen ob sich das Pattern mit anderen Pattern beißt oder nicht.

Prototype Pattern vs Clone

Häufig mache ich kein unterschied zwischen einer Kopie und einem Klon. Allerdings macht es in manchen Fällen durchaus Sinn.

Wenn das Objekt aus einer Datenbank ausgelesen wurde, beinhaltet es auch diverse IDs zu allen verwendeten Entitäten. Bei einem Klon möchte man diese unter Umständen gar nicht haben. Daher könnte man darüber nachdenken zusätzlich nach dem erstellen der Kopie die IDs auf einen Standardwert zurückzusetzen (entweder 0 oder Guid.Empty)

Prototype Pattern vs Copy Constructor

Ein copy constructor in C++ würde in etwa so aussehen (Bitte in den Kommentaren berichtigen, falls es falsch ist 🙂 ):

class Prototype 
{ 
private: 
    int state; 
public: 
    Prototype();
    Prototype(const Prototype&);
}; 
Prototype::Prototype(const Prototype& other)
{
	state = other.state;
}

int main() 
{ 
    Prototype prot1 = new Prototype();
    Prototype prot2 = prot1;
}

In C# gibt es leider (oder zum Glück) keine Möglichkeit sowas zu machen. Da würde der Aufruf etwa so aussehen:

Prototype prot2 = new Prototype(prot1);

Es gibt zwar Operatorüberladung, jedoch könnten nur unäre (z.B. + – ~ etc.), binäre (& | * / etc.) und Vergleichsoperatoren (< > == etc.) überschrieben werden. Zuweisungsoperatoren gehören hier jedoch nicht dazu und können auch nicht überschrieben werden.

Ferner müssen die Objekte auch einzeln und per Hand kopiert werden. Diese Kopie kann anschließend jedoch in der DeepCopy (oder ShallowCopy) Methode aufgerufen werden. Man sollte sich nur auf eins einigen und dies dann auch in der kompletten Domäne durchziehen.

Prototype Pattern und Factory

Protypen benötigen keine weiteren Klassen. Die Minimalversion kommt bereits mit einer Klasse aus. Es muss jedoch einmalig ein komplettes Objekt erzeugt werden.
Eine Fabrik kann die Erzeugung kapseln:

var factory = new ProductFactory()
Product product = factory.CreateNewProduct()

Benötigt dafür jedoch mindestens eine weitere Klasse.

Prototype Pattern und Abstract Factory

Abstrakte Fabriken, werden häufig mithilfe der Fabriken abgebildet. Man kann das Pattern aber auch mithilfe eines Prototyps abbilden.

Prototype Pattern und Singleton

Prototypen können als Singletons abgebildet werden.

Dies kann man entweder ganz klassisch lösen:

ProductPrototype.GetInstance().Clone()

Oder auch mithilfe von statischen Klassen

ProductPrototype.CreateNewProduct()

Prototype Pattern und Private Variablen

Machen wir es kurz.

  • Der Definition sollen auch private und protected Variablen kopiert werden.
  • MemberwiseClone kopiert auch private Variablen
  • Innerhalb der Klasse hat man Zugriff auf private Variablen einer anderen Instanz desselben Typs.

Wann kann das Prototype Pattern verwendet werden

Man kann das Pattern in Betracht ziehen, wenn:

  1. … das Erzeugen neuer Instanzen zu komplex und unübersichtlich sind (Stichwort: Don’t Repeat Yourself).
  2. … das Erzeugen neuer Instanzen unperformant ist (bspw. wenn es mit einer komplexen Datenbankabfrage behaftet ist).
  3. … das Objekt mehrere Status (bzw. Zustände) definiert, die alle mitunter relevant und gleichzeitig gebraucht werden.

Die Vorteile vom Prototype Pattern

  1. Komplexe Objekte können sehr schnell und unkompliziert erzeugt werden ohne die genaue Initialisierungsparameter kennen zu müssen.
  2. Wenn die Erzeugung einer neuen Instanz kostspielig ist, kann das kopieren einen positiven Nebeneffekt auf die Performance haben.
  3. Dadurch, das weitere Erzeugungsmethoden (als Beispiel: Factory) entfallen und die jeweilge Klasse sich selber kopieren kann, bedarf es keine weiteren Typen. Die Anzahl an Klassen kann dezimiert werden. Eine gute Architektur misst sich nicht an der Anzahl der Klassen, sondern an der Lesbarkeit und Wartbarkeit.

Die Nachteile vom Prototype Pattern

  1. Es gibt keinen Zugriff auf den Konstruktor. Nicht auf den Konstruktor zugreifen zu können, kann aber auch ein Nachteil sein. Manchmal werden gerade im Konstruktor wichtige Informationen zum Erzeugen einer neuen Instanz essentiell wichtig sind.
  2. Komplexe Strukturen sind aufwendiger zu kopieren, da jedes Objekt eine Möglichkeit zum kopieren bereitstellen muss. Besonders schwierig wird es, wenn man keinen Einfluss auf die zu kopierenden Klassen hat.

Mein Fazit zum Prototype Pattern

Fassen wir noch einmal kurz zusammen.

Per Definition, wird ein prototypisches Objekt erstellt um es später immer und immer wieder zu klonen und mit weiteren Parametern zu modifizieren und zu verändern.

In der Praxis werden Objekte jedoch sehr schnell mit dem Schlüsselwort new erzeugt. Dies ist auch viel schneller und bequemer als eine komplette Kaskade an Klonen zu erstellen.

In der Tat findet das Pattern in der Praxis auch nur sehr wenig Anwendung. Ich persönlich habe bereits mehrere Millionen Zeilen Quellcode geschrieben. Die Notwendigkeit es einzusetzen kann ich aber an einer Hand abzählen.

  1. Ich wollte mal ein spezielles Objekt serialisieren und in einem UnitTest als Testdatensatz verwenden (800kB sind nun mal kleiner als 200MB an Datenbank).
  2. Ebenfalls musste ich einmal den Cache von NHibernate auszuhebeln.
  3. Der Klassische „Back-Button“

Zum Schluss möchte ich noch erwähnen: Es ist ein Werkzeug.

Ein Werkzeug kann das Leben leichter machen. Muss es aber nicht, wenn es falsch eingesetzt ist.

Man kann mit einer Zange ein Nagel in die Wand schlagen.

Das geht!

Ein Hammer wäre aber effektiver 🙂