Die Klasse sieht etwa wie folgt aus:
{
public IContainer Container { get; private set; }
public int ImutableValue1 { get; private set; }
public string ImutableValue2 { get; private set; }
public int MutableValue3 { get; private set; }
public string MutableValue4 { get; private set; }
public MyClass(IContainer container, int imutableValue1 = 1, string imutableValue2 = "a", int mutableValue3 = 2, string mutableValue4 = "b")
{
Container = container;
ImutableValue1 = imutableValue1;
ImutableValue2 = imutableValue2;
MutableValue3 = mutableValue3;
MutableValue4 = mutableValue4;
if(container != null)
{
container.Register(this);
}
}
}
Um in die neue Struktur hinzugefügt zu werden muss der container
gesetzt werden. Die anderen Werte können gesetzt werden, ansonsten wird der Default-Wert übernommen.
Für das Erstellen der Kopie gab es am Anfang folgenden Code, der im Client angesiedelt war:
{
// Do some things
foreach(var myClass in source.MyClassList)
{
var newClass = new MyClass(newContainer, myClass.ImutableValue1, myClass.ImutableValue2, myClass.MutableValue3, myClass.MutableValue4);
}
// Do some other things
}
Dieser Code beinhaltet folgende Probleme:
- Der Client hat bzw. benötigt zu viel Wissen über die Klasse.
- Wenn ein Property ändert oder ein neues hinzukommt müssen alle Stellen, an welchen dieser Kopier-Code vorhanden ist, angepasst werden.
- Die Gefahr ist gross, dass bei einem neuen Property einige Stellen vergessen werden und dann in der Kopie immer der Default-Wert (und nicht der Instanz-Wert) eingetragen wird. Je nach Tests und Test-Abdeckung kann dies erst sehr spät, sprich erst im regulären Betrieb, auffallen.
Auf der Suche nach einer Lösung haben wir verschiedene Ansätze diskutiert.
Copy Constructor
Der Copy Constructor ist besonders aus der C++-Welt bekannt. Dort ist immer mindestens ein (impliziter) Copy Constructor vorhanden.
Für das oben gezeigte Beispiel würde der Copy Constructor etwa wie folgend aussehen:
{
// ...
public MyClass(MyClass source, IContainer newContainer, int? mutableValue3 = null, string mutableValue4 = null)
{
Container = newContainer;
ImutableValue1 = source.ImutableValue1;
ImutableValue2 = source.ImutableValue2;
MutableValue3 = mutableValue3 != null ? mutableValue3 : source.MutableValue3;
MutableValue4 = mutableValue4 != null ? mutableValue4 : source.MutableValue4;
if(Container != null)
{
Container.Register(this);
}
}
}
Der Client-Code würde dann so aussehen:
{
// Do some things
foreach(var myClass in source.MyClassList)
{
var newClass = new MyClass(myClass, newContainer);
}
// Do some other things
}
Clone()-Funktion
Im C#-Umfeld ist die Clone-Funktion verbreiteter als der Kopierkonstruktor, besonders durch das Interface IClonable des .NET-Frameworks. Da wir aber keine 1:1-Kopie der Objekt-Instanz erstellen wollen können wir das Interface hier nicht einsetzen.
Die Implementation unserer Clone-Funktion könnte etwa so aussehen:
{
// ...
public MyClass Clone(IContainer newContainer, int? mutableValue3 = null, string mutableValue4 = null)
{
var newClass = new MyClass(
newContainer,
ImutableValue1,
ImutableValue2,
mutableValue3 != null ? mutableValue3 : MutableValue3,
mutableValue4 != null ? mutableValue4 : MutableValue4
);
return newClass;
}
}
Mit der Clone()-Funktion würde sich dann der Client-Code wie folgt präsentieren:
{
// Do some things
foreach(var myClass in source.MyClassList)
{
var newClass = myClass.Clone(newContainer);
}
// Do some other things
}
Factory
Die dritte Variante besteht aus einer Factory, die sich um die Erzeugung der MyClass-Instanzen kümmert. Der Konstruktor der Klasse MyClass sollte dann auf internal
gesetzt werden um die Erzeugung über die Factory zu erzwingen.
{
public static MyClass CreateMyClass(IContainer container, int imutableValue1 = 1, string imutableValue2 = "a", int mutableValue3 = 2, string mutableValue4 = "b")
{
var myClass = new MyClass(container, imutableValue1, imutableValue2, mutableValue3, mutableValue4);
return myClass;
}
public static MyClass CopyMyClass(MyClass source, IContainer newContainer, int? mutableValue3 = null, string mutableValue4 = null)
{
var newClass = new MyClass(
newContainer,
source.ImutableValue1,
source.ImutableValue2,
mutableValue3 != null ? mutableValue3 : source.MutableValue3,
mutableValue4 != null ? mutableValue4 : source.MutableValue4
);
return newClass;
}
}
public class MyClass
{
// ...
internal MyClass(IContainer container, int imutableValue1 = 1, string imutableValue2 = "a", int mutableValue3 = 2, string mutableValue4 = "b")
{
// ...
}
}
Für den Einsatz der Factory wäre folgender Client-Code nötig:
{
// Do some things
foreach(var myClass in source.MyClassList)
{
var newClass = MyClassFactory.CopyMyClass(myClass, newContainer);
}
// Do some other things
}
Vergleich
Der Kopierkonstruktor und die Clone-Funktion haben beide den Nachteil, dass der Benutzer der Klasse die Varianten schnell übersehen kann. Dadurch würde trotzdem Code wie im anfänglichen Client-Code-Beispiel geschrieben. Durch das Verschieben der Erzeugungs-Funktionen in eine Factory sind die Varianten der Erzeugung schnell ersichtlich und der Benutzer kann so die passende Variante wählen.
Aus diesem Grund haben wir dann auch die Variante mit der Factory gewählt.