Prototyp (Prototype)

Prototyp (Prototype)

Prototyp jest kreacyjnym wzorcem projektowym, który umożliwia kopiowanie już istniejących obiektów. To dość prosty wzorzec.

Cel

Celem wzorca jest określenie rodzajów obiektów do utworzenia przy użyciu instancji prototypowej i utworzenie nowych obiektów poprzez skopiowanie tego prototypu. Zasadniczo opisuje, jak klonować. Innymi słowy, pozwala nam kopiować istniejące obiekty bez uzależniania kodu klienta od jego konkretnych klas.

Problem

Wyobraźmy sobie, że mamy szkołę, w której pracujemy z ludźmi.

Możemy po prostu tworzyć nowe osoby, tworząc nowe instancje klasy Person. Natomiast bardziej realistycznie, zakładając, że klasa Person jest klasą abstrakcyjną, z której dziedziczą klasy Student i Teacher możemy tworzyć nowych studentów lub nauczycieli. Teraz wyobraź sobie przypadek użycia, w którym większość wartości właściwości lub pól istniejącego obiektu jest wymagana w nowym obiekcie. A może potrzebujesz nawet wszystkich wartości właściwości nowego obiektu, aby pasowały do istniejącego obiektu. Co możemy zrobić w takim przypadku.

Rozwiązanie

W takim przypadku możemy je po prostu skopiować. Ale to wymaga od klienta wiedzy na temat konkretnych klas, z którymi pracujemy (Student, Teacher). Musimy też wiedzieć, jak je tworzyć, ale możemy to również zrobić w inny sposób.

Wzorzec projektowy Prototyp deleguje proces klonowania samym obiektom, które mają być sklonowane. We wzorcu tym deklarowany jest wspólny interfejs dla wszystkich obiektów wspierających funkcjonalność klonowania. Interfejs taki pozwala klonować obiekty bez konieczności sprzęgania kodu z klasą obiektu. Zazwyczaj taki interfejs ogranicza się tylko do pojedynczej metody Clone.

Moglibyśmy stworzyć metodę Clone na naszych klasach Student i Teacher, która zwróci kopię. Co więcej, jak wiemy, zarówno Student, jak i Teacher wywodzą się od klasy Person.

Czyli możemy tę metodę zdefiniować w klasie Person i nadpisać ją w podklasach. Implementacja metody Clone jest bardzo podobna w poszczególnych klasach.

Obiekt, który posiada, funkcjonalność klonowania zwany jest prototypem. Gdy obiekty, z którymi masz, do czynienia mają, mnóstwo pól i setki możliwych konfiguracji, klonowanie ich może okazać się korzystną alternatywą do tworzenia podklas.

Natomiast aplikacja kliencka musi znać tylko metodę Clone klasy Person, aby móc klonować podklasy, takie jak Student i Teacher. To, co tu widzisz, to działający wzór prototypu. W istocie opisuje sposób tworzenia nowego obiektu poprzez klonowanie istniejącego. Odwzorujmy ten przykład na strukturę wzorca.

Struktura

Wzorzec prototype oczywiście zawiera klasę Prototype. Ten prototyp deklaruje oczywiście interfejs do klonowania.

W naszym przykładzie jest to klasa Person i sposobem na zaimplementowanie tego była abstrakcyjna klasa bazowa z abstrakcyjną metodą Clone. I jak to zazwyczaj bywa, jeśli chcesz, zastosować interfejs też tak możesz zrobić.

Następnie mamy ConcretePrototype.

Są to implementacje prototypu, które implementują rzeczywistą operację klonowania, która zwraca kopię samego siebie. W naszym przykładzie są to klasy Student i Teacher.

Następnie mamy klienta, który tworzy nowy obiekt, prosząc Prototype o sklonowanie samego siebie. Jedyną rzeczą, którą musimy zmienić w naszej strukturze wzorca prototypu, jest to, że klient nie wykonuje klonowania na Person, ale robi to na Prototype.

I to wszystko. Zaimplementujmy to teraz w kodzie.

Przykład implementacji

Zaczynamy od prototypu i wykorzystamy do tego abstrakcyjną klasę bazową.

/// <summary>
/// Prototype
/// </summary>
public abstract class Person
{
   public abstract int Id { get; set; }
   public abstract string Name { get; set; }

   public abstract Person Clone();
}

Definiujemy właściwość Id i Name oraz metodę Clone, która zwraca obiekt Person, czyli prototyp, czyli klon samego siebie.

A teraz musimy to wdrożyć. Zacznijmy od klasy Teacher wywodzącej się z klasy Person. Oznacza to, że musimy zaimplementować abstrakcyjną klasę bazową, więc zróbmy to.

/// <summary>
/// ConcretePrototype1
/// </summary>
public class Teacher : Person
{
   public override int Id { get; set; }
   public override string Name { get; set; }

   public Teacher(int id, string name)
   {
      Id = id;
      Name = name;    
   }

   public override Person Clone()
   {
      return (Person)MemberwiseClone();
   }

   public override string ToString()
   {
      return $"Sklonowany: Nauczyciel\t{Name} o id {Id}";
   }
}

Widzimy, że musimy podać Id i Name. Możemy w tym celu dodać konstruktor, który przyjmuje id i name, co oznacza, że oczekujemy, że obiekt Nauczyciel zostanie skonstruowany z id i nazwą. Następnie musimy również zaimplementować metodę Clone. Moglibyśmy to zaimplementować, dodając nową osobę i ręcznie kopiując wszystkie pola, ale istnieje łatwiejszy, wbudowany sposób, aby to zrobić, wywołując MemberwiseClone.

MemberwiseClone jest to metoda zdefiniowana na obiekcie. Tak więc każda instancja, której używamy w C#, ma do tego dostęp. MemberwiseClone tworzy płytką kopię istniejącego obiektu. Po tym demo wytłumaczę, co oznacza płytka kopia. Na razie to wystarczy dla naszej klasy Teacher. I jeszcze nadpisałem, metodę ToString, abyśmy mogli łatwiej zobaczyć efekt naszej pracy.

Następnie dodajmy kolejny konkretny prototyp, Student.

/// <summary>
/// ConcretePrototype2
/// </summary>
public class Student : Person
{
   public override int Id { get; set; }
   public override string Name { get; set; }
   public Teacher Teacher { get; set; }

   public Student(int id, string name, Teacher teacher)
   {
      Id = id;
      Name = name;
      Teacher = teacher;
   }

   public override Person Clone()
   {
      return (Person)MemberwiseClone();
   }
   
   public override string ToString()
   {
      return $"Sklonowany: Student\t{Name} o id {Id} ze swoim nauczycielem  {Teacher.Name} o id {Teacher.Id}";
   }
} 

To mniej więcej to samo, co klasa Teacher, z jedną różnicą. Klasa student ma dodatkową właściwość, jaką jest Teacher. Teraz możemy to przetestować i przechodzimy do klasy progrm.cs

using Prototype;
Console.Title = "Prototype";

var teacher = new Teacher(1, "Marcin");
var teacherClone = (Teacher)teacher.Clone();
Console.WriteLine(teacherClone);

var student = new Student(2, "Tomasz", teacher);
var studentClone = (Student)student.Clone();
Console.WriteLine(studentClone);

Console.ReadKey();

Zaczynamy od utworzenia nowego nauczyciela (Teacher), któremu ustawiamy id na 1 i name na Marcin. Następnie klonujemy tego nauczyciela. W tym celu wywołujemy metodę Clone na obiekcie teacher. Zauważ, że musimy rzutować to z powrotem do teacher, jeśli chcemy użyć go jako nauczyciela, ponieważ metoda Clone zwraca obiekt Person, abstrakcyjną klasę bazową. I wyswietlamy wynik klonowania.

Następnie robimy to samo dla studenta (Student). Różnica polega na tym, że do konstruktora przekazujemy nauczyciela, którego właśnie stworzyliśmy. I teraz możemy to uruchomić.

Wynik działania

Sklonowany: Nauczyciel Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Marcin o id 1

I wydaje się, że to działa dobrze. Imię klona nauczyciela zostało wypisane i to jest Marcin, co oznacza, że klonowanie menedżera wydaje się działać. Student również został sklonowany, to Tomasz, a jego nauczyciel to Marcin. Czyli wszystko jest dobrze. Jeśli zamiast teacher przekażemy teacherClone, powinniśmy zobaczyć dokładnie ten sam wynik.

var student = new Student(2, "Tomasz", teacherClone);

Spróbujmy to uruchomić i rzeczywiście dostajemy taki sam wynik. Wydaje się, że to działa dobrze.

Wynik działania

Sklonowany: Nauczyciel Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Marcin o id 1

Ale zróbmy jedną małą zmianę. Tuż przed instrukcją ReadKey zmieńmy name nauczyciela i ponownie wypiszmy dane studenta.

teacherClone.Name = "Wanda";
Console.WriteLine(studentClone);

Spróbujmy to uruchomić.

Wynik działania

Sklonowany: Nauczyciel Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Wanda o id 1

I to trochę dziwne. Nazwa nauczyciela w klonie studenta została zmieniona, mimo że klon został utworzony przed zmianą nazwy. Czasami możesz tego chcieć, ale często tak nie jest. Spodziewasz się, że po sklonowaniu obiektu studenta otrzymasz również rzeczywistą kopię obiektu nauczyciel. A tutaj tak nie jest, i to jest ograniczenie MemberwiseClone, które tworzy płytkie kopie. Uzyskanie rzeczywistej kopii obiektu zarządzającego również oznaczałoby, że potrzebujemy wsparcia dla głębokich kopii.

Płytka kopia kontra głęboka kopia

Płytka kopia jest to kopia wartości typu pierwotnego. Tak więc zostaną skopiowane stringi, liczby całkowite i tak dalej. Są one niezależne od oryginalnego obiektu. Jednak typy złożone, które mogą być właściwościami obiektu, który klonujesz, będą współużytkowane przez różne klony. Z tego powodu zmiana wartości tego złożonego obiektu, na przykład nazwy nauczyciela, powoduje zmiany zarówno w oryginalnym obiekcie, jak i w klonie.

Z drugiej strony głęboka kopia powoduje również kopie typów złożonych. W takim przypadku możemy zmodyfikować złożone właściwości oryginalnego obiektu bez wpływu na właściwości sklonowanego obiektu i odwrotnie. Zobaczmy, jak możemy to zrobić w naszym kodzie.

Przykład implementacji

Dodamy teraz obsługę głębokich kopii. Zacznijmy od dodania wartości logicznej deepClone jako parametru do sygnatury metody klonowania. Robimy to w abstrakcyjnej klasie bazowej Person, co oznacza, że musimy to zrobić również w klasie Teacher i klasie Student. W klasach implementujących dobrze jest przekazywać dla tego wartość domyślną, false, aby klienci nie mieli obowiązku mówienia o tym wprost.

public abstract Person Clone(bool deepClone);
public override Person Clone(bool deepClone = false);
public override Person Clone(bool deepClone = false);

Wszystko, co pozostało, to stworzenie to głębokiego klonowanie. Moglibyśmy ręcznie przeszukiwać drzewo obiektów, tworząc płytkie kopie każdego złożonego typu i przypisując je jako wartość właściwości.

Ale bardziej powszechnym podejściem jest użycie JsonSerializer. W System.Text.Json jest wbudowany, ale obecnie nie obsługuje klas konstruktorów bez parametrów i właśnie to mamy tutaj. Ale na szczęście jest Json.NET tak, więc zainstalujmy, go korzystając z NuGeta i poszukajmy Newtonsoft.Json. Zainstalujmy go i gotowe. I teraz możemy to zaimplementować w Teacher.

public override Person Clone(bool deepClone = false)
{
   if (deepClone)
   {
      var objectAsJson = JsonConvert.SerializeObject(this);
      return JsonConvert.DeserializeObject<Teacher>(objectAsJson);
   }

   return (Person)MemberwiseClone();
}

Musimy zaimportować przestrzeń nazw Newtonsoft.Json. Od tego momentu możemy wywoływać SerializedObject na naszym bieżącym obiekcie, a następnie deserializować ten obiekt. Więc najpierw dokonujemy serializacji do ciągu, a następnie deserializujemy ten ciąg z powrotem do obiektu, który możemy zwrócić. Ostrzeżenie, które tutaj widzisz, mówi, że może to zwrócić wartość null, ale to jest dla nas w porządku.

I to samo robimy w klasie Student. Ale tym razem dokonujemy deserializacji do Studenta.

public override Person Clone(bool deepClone = false)
{
   if (deepClone)
   {
      var objectAsJson = JsonConvert.SerializeObject(this);
      return JsonConvert.DeserializeObject<Student>(objectAsJson);
   }

   return (Person)MemberwiseClone();
}

I przetestujmy to. Aby to zrobić, zmieniamy nasze wywołanie klonów, tym razem prosząc o głęboki klon. Robimy to wywołując student.Clone i przekazujac true.

var studentClone = (Student)student.Clone(true);

Spróbujmy to uruchomić.

Wynik działania

Sklonowany: Nauczyciel Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Marcin o id 1
Sklonowany: Student Tomasz o id 2 ze swoim nauczycielem Marcin o id 1

Tym razem otrzymujemy oczekiwany rezultat. Zmieniliśmy nazwę nauczyciela po sklonowaniu studenta, ale klonowanie studenta sklonowało również obiekt nauczyciela, co oznacza, że widzimy nazwę nauczyciela klonu, a nie zmianę nazwy, którą zrobiliśmy później. W rzeczywistości jest to głęboki klon.

Zastosowanie

Stosuj wzorzec Prototyp, gdy chcesz, aby twój kod nie był zależny od konkretnej klasy kopiowanego obiektu. Sytuacja taka zdarza się często, gdy twój kod pracuje na obiektach przekazanych z zewnętrznego źródła poprzez jakiś interfejs. Nieznana jest wówczas konkretna klasa takich obiektów. Wzorzec Prototyp pozwala kodowi klienckiemu na pracę ze wszystkimi obiektami wspierającymi klonowanie za pomocą uogólnionego interfejsu. Interfejs czyni kod kliencki niezależnym od konkretnych klas klonowanych obiektów.

Stosuj ten wzorzec, gdy chcesz zredukować ilość podklas różniących się jedynie sposobem inicjalizacji swych obiektów. Ktoś inny bowiem mógł stworzyć takie podklasy tylko w celu tworzenia obiektów o określonej konfiguracji. Wzorzec Prototyp pozwala korzystać z zestawu prefabrykowanych obiektów w różnorakich konfiguracjach, stanowiących prototypy. Zamiast tworzyć instancję podklasy zgodnej z jakąś konfiguracją, klient może po prostu wyszukać i sklonować odpowiedni prototyp.

Zalety i wady

  • Dużo wygodniejsze produkowanie złożonych obiektów.
  • Możesz klonować obiekty bez konieczności sprzęgania ze szczegółami ich konkretnych klas.
  • Możesz pozbyć się wielokrotnie powtarzanego kodu inicjalizacyjnego na rzecz klonowania prefabrykowanych prototypów.
  • Podejścieto stanowi alternatywę do dziedziczenia w przypadku gdy mamy do czynienia z wcześniej zdefiniowanymi konfiguracjami złożonych obiektów.
  • Klonowanie złożonych obiektów, które mają odniesienia cykliczne, może być trudne.

Kod źródłowy

1 comment

  1. Twój blog to skarbnica wiedzy. Zawsze tutaj wracam w poszukiwaniu rozwiązań.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *