CloneWzór.17 Prototype jest wzorcem, którego zadaniem jest stworzenie swoich kopii do dalszej modyfikacji. Jest to ostatni wzorzec z kategorii "Creational Patterns", który został opisany na tym blogu. 

Prototype może być użyty w każdej klasie. Wystarczy w niej stworzyć metodę, która umożliwi stworzenie dokładnej kopii tego obiektu.

Musisz przyznać, że pomysł na szybkie tworzenie obiektu jest ciekawy. Zamiast tworzyć jakieś Fabryki, Budowniczych, dlaczego po prostu nie klonować istniejących obiektów i tak przyspieszać ich tworzenie w kodzie.

Sam wzorzec projektowy Prototype. Nie wymaga jakieś diagramu UML. Szczerze to patrzenie na takie coś może bardziej cię skołować.

Diagram UML wzorca Prototype

Esencja tego wzorca polega na tym, aby mieć interfejs, w który jest metoda "Clone()". Gdy dana klasa implementuję tę interfejs i tę metodę znaczy to, że możesz ją klonować i powielać.

Wyzwanie więc tego wzorca nie siedzi w jego złożoności, tylko w implementacji samego klonowania, kopiowania obiektów.

Wraz z C# 9.0 i wyżej pojawiły się także Rekordy. Rekordy mają swoje własne słowo kluczowe "with", które domyślnie rozwiązują problem wzorca Prototype, jak i klonowania.

Na to też dzisiaj spojrzymy.

Kopiowanie Deep vs Shallow

Pierwszy problem związany z kopiowaniem polega na głębokości samego kopiowania. 

Kopiowanie płytkie tylko sklonowało obiekt po prostu przypisując do niego pola i właściwości.

var a1 = new Exam() { B = 10, C = 11};

var a2 = new Exam() { B = a1.B, C = a1.C };

public class Exam
{
    public int B { get; set; }
    public int C { get; set; }
}

Takie kopiowanie mogłoby być wystarczającego, gdyż wszystkie właściwości z tego przykładu są typem wartościowym. Dla typów wartościowych przyrównanie kopiuje wartość.

Sytuacja się jednak zmienia, gdy mamy złożone obiekty, które zawierają w sobie kolejne typy referencyjne.

var a1 = new Exam() { B = 10, C = new Inner() { D = 11 } };

var a2 = new Exam() { B = a1.B, C = a1.C };

public class Exam
{
    public int B { get; set; }
    public Inner C { get; set; }
}

public class Inner
{
    public int D { get; set; }
}

W wyniku kopiowania płytkiego teraz właściwość "C" tak naprawdę nie jest kopią czy klonem. Przypisaliśmy teraz tylko adres do tego obiektu i obie instancje "Exam" odnoszą się do tego samego obiektu w swojej właściwości "C".

Możemy to nawet potwierdzić pisząc proste sprawdzenie.

Wynik kodu

W takim wypadku musielibyśmy zrobić głębokie kopiowanie, czyli zaglądać do wszystkich właściwości obiektów w obiekcie i tak tworzyć prawdziwą kopię.

var a1 = new Exam() 
{   B = 10, 
    C = new Inner() 
    { 
        D = 11 
    } 
};

var a2 = new Exam()
{
    B = a1.B,
    C = new Inner()
    {
        D = a1.C.D
    }
};

Jeśli więc chcemy skorzystać ze wzorca Prototype to warto się zastanowić, które klonowanie jest nam potrzebne. Kto wie być może celowo chcesz zrobić płytkie kopiowanie.

Interfejs ICloneable, który jest słaby

Framework .NET oferuje swój interfejs "ICloneable" do tego zadania. Ten interfejs daje nam jedną metodę "Clone", która zwraca obiekt. 

To jest jeden z problemów tego interfejsu. Drugi problem polega na tym, że sam interfejs, jak i dokumentacja do niego nie określa jaki typ kopii ma ona zwrócić.

Oto przykład domu, który zawiera złożony obiekt adresu. Jego implementacja tego interfejsu wygląda tak.

public class House : ICloneable
{
    public string Name { get; set; }
    public  Address Address { get; set; }
    public House(string name, Address address) 
    {
        Name = name;
        Address = address;
    }

    public object Clone()
    {
        return (House)MemberwiseClone();
    }
}

Metoda "MemberwiseClone()" pochodzi z klasy Object i w sumie każda klasa może z niej skorzystać. 

Ta metoda stworzy "płytką" kopię. W .NET nie ma gotowej metody do robienia głębokiej kopii.

public class Address
{
    public  string StreetName { get; set; }
    public int HouseNumber { get; set; }
    public Address(string streetName, int houseNumber) 
    {
        StreetName = streetName;
        HouseNumber = houseNumber;
    }
}

Analogicznie dodajemy implementacje tego interfejsu do klasy reprezentującą Adres.

Na koniec zostało nam tylko wykazać, że rzeczywiście metoda "MemberwiseClone()" tworzy płytką kopię.

var annaHouse = new House(
"Anna House",
new Address("Lemonowa 12", 365));

var annaHouseClone = annaHouse.Clone() as House;

bool check = annaHouseClone.Name == "Anna House";
bool check2 = annaHouseClone.Address.HouseNumber == 365;

Console.WriteLine("Before the change");
Console.WriteLine("Clone House");
Console.WriteLine(check);
Console.WriteLine("Clone Address");
Console.WriteLine(check2);

annaHouse.Name = "Test";
annaHouse.Address.HouseNumber = 0;
annaHouse.Address.StreetName = "streettest";

bool check3 = annaHouseClone.Name == "Anna House"; //will be ok

bool check4 = annaHouseClone.Address.HouseNumber == 365; //it will fail because of Shallow Copy

Console.WriteLine("After change");
Console.WriteLine("Clone House");
Console.WriteLine(check3);
Console.WriteLine("Clone Address");
Console.WriteLine(check4);

Zróbmy więc to lepiej.

Napiszmy głębokie klonowanie sami

Po pierwsze stwórzmy interfejs, który jawnie mówi nam jaką kopię chcemy zrobić. 

interface IDeepCopyable<T>
{
    T DeepCopy();
}

Dzięki temu, że jest to interfejs generyczny możemy teraz zwrócić konkretną klasę, a nie typ ogólny do wszystkiego "object".

public class House : IDeepCopyable<House>
{ 
    public House DeepCopy()
    {
        var copy = new House();
        copy.Name = (String)Name.Clone();
        copy.Address = Address.DeepCopy();
        return copy;
    }

    public string Name { get; set; }
    public Address Address { get; set; }
    public House(string name, Address address) 
    {
        Name = name;
        Address = address;
    }

    public House()
    {
    }
}

W adresie także zaimplementujemy ten interfejs

public class Address : IDeepCopyable<Address>
{
    public Address DeepCopy()
    {
        return new Address(StreetName, HouseNumber);
    }

    public string StreetName { get; set; }
    public int HouseNumber { get; set; }
    public Address(string streetName, int houseNumber) 
    {
        StreetName = streetName;
        HouseNumber = houseNumber;
    }
}

Teraz gdy robimy głęboką kopię wszystkie flagi sprawdzające dadzą nam prawdę.

var annaHouse = new House(
"Anna House",
new Address("Lemonowa 12", 365));

var annaHouseClone = annaHouse.DeepCopy();

bool check = annaHouseClone.Name == "Anna House";
bool check2 = annaHouseClone.Address.HouseNumber == 365;

Console.WriteLine("Before the change");
Console.WriteLine("Clone House");
Console.WriteLine(check);
Console.WriteLine("Clone Address");
Console.WriteLine(check2);

annaHouse.Name = "Test";
annaHouse.Address.HouseNumber = 0;
annaHouse.Address.StreetName = "streettest";

bool check3 = annaHouseClone.Name == "Anna House"; //will be ok

bool check4 = annaHouseClone.Address.HouseNumber == 365; //it will be ok beacuse of DeepCopy

Console.WriteLine("After change");
Console.WriteLine("Clone House");
Console.WriteLine(check3);
Console.WriteLine("Clone Address");
Console.WriteLine(check4);

A może Copy Constructor?

Do problemu klonowania można też podjeść inaczej. Możemy użyć techniki z C++, która nazywa się copy constructor.

Po prostu tworzymy konstruktor, który przyjmuje inny wersję swojej klasy i robi z niego kopię.

public class Address 
{
    public Address(Address other)
    {
        StreetName = other.StreetName;
        HouseNumber = other.HouseNumber;
    }

    public string StreetName { get; set; }
    public int HouseNumber { get; set; }
    public Address(string streetName, int houseNumber) 
    {
        StreetName = streetName;
        HouseNumber = houseNumber;
    }
}

Oczywiście z tą techniką zaczynają się schody, gdy zdajesz sobie sprawę, że każda twoja klasa zawarta w takim łańcuchy musi mieć także taki konstruktor.

public class House 
{
    public House(House other)
    {
        Name = (String)other.Name.Clone();
        Address = new Address(other.Address);
    }

    public string Name { get; set; }
    public Address Address { get; set; }
    public House(string name, Address address) 
    {
        Name = name;
        Address = address;
    }

    public House()
    {
    }
}

Będą zmuszony tworząc takie konstruktory łamiesz zasadę Otwarte-Zamknięte ("Open-Close"). Dlatego podejście prototype z interfejsem jest lepsze.

Prototype Factory

Dlaczego nie połączyć dwóch wzorców projektowych i nie stworzyć fabryki, która będzie Ci wypluwać gotowe obiekty.

Oto zbiór klas do tego przykładu:

public class OperationStatus : IDeepCopyable<OperationStatus>
{
    public OperationStatus(string nameOfOperation, Info info)
    {
        NameOfOperation = nameOfOperation;
        Information = info;
    }

    public string NameOfOperation { get; set; }

    public bool IsRed { get; set; }

    public Info Information { get; set; }

    public OperationStatus DeepCopy()
    {
        throw new NotImplementedException();
    }
}

public class Info : IDeepCopyable<OperationStatus>
{
    public Info(string messaage,string stack, int time)
    {
        Message = messaage;
        Stack = stack;
        Time = time;
    }

    public string Message { get; set; }

    public string Stack { get; set; }

    public int Time { get; set; }

    public OperationStatus DeepCopy()
    {
        throw new NotImplementedException();
    }
}

A oto nasza fabryka, która ma zestaw metod statycznych, które wypluwają gotowe i wypełnione odpowiednio statusy procesów.

public class OperationStatusFactory
{
    private static OperationStatus main =
    new OperationStatus(null, new Info("WCF", ".NET", 0));
    private static OperationStatus java =
    new OperationStatus(null, new Info("REST", "JAVA", 0));

    public static OperationStatus NewRedDotNetStatus(string name, int
    time) =>
    NewStatus(main, name, time, true);

    public static OperationStatus NewGreenJavaStatus(string name, int
    time) =>
    NewStatus(java, name, time, false);

    private static OperationStatus NewStatus(OperationStatus proto, string name,
    int time, bool isred)
    {
        var copy = proto.DeepCopy();
        copy.NameOfOperation = name;
        copy.Information.Time = time;
        copy.IsRed = isred;
        return copy;
    }
}

Gdy wiesz, że w twoim programie będzie potrzebował gotowe sposoby na wypluwanie podobnych obiektów z małymi różnicami to "Prototype Factory" idzie z pomocą.

Serializacja jako metoda klonowania

Jak można proces głębokiego klonowania zrobić lepiej?  W trakcie serializacji obiektu zapisujesz wszystkiego danego do jakiego strumienia np. pliku XML albo pamięci.

Gdy go deserializujesz go z tego strumienia to oczywiście otrzymasz w ten sposób jego głęboką kopię.

Możesz ten fakt wykorzystać do tworzenia głębokiej kopii celowo.

public static class Tools
{
    public static T DeepCopy<T>(this T self)
    {
        using (var stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, self);
            stream.Seek(0, SeekOrigin.Begin);
            object copy = formatter.Deserialize(stream);
            return (T)copy;
        }
    }
} 

Istnieje oczywiście tutaj jeden problem. Każda klasa, która chce być tak klonowała musi mieć atrybut [Serializable] 

var annaHouse = new House(
"Anna House",
new Address("Lemonowa 12", 365));

var copy = annaHouse.DeepCopy<House>();

var anonim = new { test = "test", test2 = 1 };

var copy2 = anonim.DeepCopy();

Na szczęście możemy użyć innej techniki serializacji, która tego atrybutu nie wymaga.

public static T DeepCopy2<T>(this T self)
{
    using (var stream = new MemoryStream())
    {
        XmlSerializer s = new XmlSerializer(typeof(T));
        s.Serialize(stream, self);
        stream.Position = 0;
        return (T)s.Deserialize(stream);
    }
}

Z rekordami czy prototype jest nam potrzebny

Wzorzec Prototype jak widzisz był jednym z lepszych sposób na rozwiązanie problemu kopii i klonowania.

Zobaczyłeś właśnie jak można go zaimplementować. Czy istnieje obecnie coś lepszego? 

W C# 9.0 pojawiły się rekordy a wraz z nimi słowo kluczowe "with", które umożliwia szybkie kopiowanie obiektów.

PrototypeG prop = new("Andy", 2865, "Red");

var prop2 = prop with { serialnumber = 1203 };
var prop3 = prop with { serialnumber = 1855 };

record PrototypeG(string name, int serialnumber, string color);

Do każdej takiej kopii możemy dodać swoje małe oprawki. Jeśli pracujesz z najnowszą wersją C# to może, zamiast korzystać ze wzorca Prototype skorzystać po prostu z rekordów.

W tym wpisie opisałem czym się różni rekord od klasy. 

Twoja metoda "with" dla klas

Dla klas słowo kluczowe "with" nie istnieje. Dla struktur od C# 10 już te słowo kluczowe jest.

Nic jednak nie stoi na przeszkodzie, aby napisać swoje własne metody rozszerzające z nazwą "with", które oferują klonowanie z małymi modyfikacjami danego obiektu.

public static class Extensions
{
    public static House With(this House house)
    public static House With(this House house, string name)
    public static House With(this House house, Address adress)
    public static Address With(this Address adress)
    public static Address With(this Address adress,string streetName)
    public static Address With(this Address adress, int houseNumber)
}

Metod powinno być tyle ile jest możliwych zmian więc te podejście może Cię przerosnąć. 

Oto ciało tych metod dla klasy reprezentującą dom.

public static House With(this House house)
{
    var copy = new House();
    copy.Name = (String)house.Name.Clone();
    copy.Address = house.Address.With();
    return copy;
}

public static House With(this House house, string name)
{
    var copy = new House();
    copy.Name = name;
    copy.Address = house.Address.With();
    return copy;
}

public static House With(this House house, Address adress)
{
    var copy = new House();
    copy.Name = (String)house.Name.Clone();
    copy.Address = adress;
    return copy;
}

Oto ciało tych metod dla klasy reprezentującą adres.

public static Address With(this Address adress)
{
    return new Address(adress.StreetName, adress.HouseNumber);
}
public static Address With(this Address adress,string streetName)
{
    return new Address(streetName, adress.HouseNumber);
}

public static Address With(this Address adress, int houseNumber)
{
    return new Address(adress.StreetName, houseNumber);
}

Teraz możemy szybko klonować obiekty, dodając do nich małe modyfikacje.

var annaHouse = new House(
"Anna House",
new Address("Lemonowa 12", 365));

var clone1 = annaHouse.With();
var clone2 = annaHouse.With("New Name");

var clone3 = annaHouse.With(
    new Address("test",11));

A może paczka NuGet?

Istnieje szereg paczek NuGet, których celem jest wykonać kopiowanie płytkie lub głębokie.

Paczki NuGet związane z kopiowanie i klonowaniem

Nie miałem okazji ich sprawdzić więc nie wiem, która jest najlepsza. Po prostu pamiętaj, że masz taką możliwość i nie musisz pisać swojej implementacji kopiowania.

Wszystkie przykłady znajdziesz tutaj na GitHub :👇

PanNiebieski/DesignPatternsInCSharp (github.com)