Wzorzec Prototyp z MemberwiseClone() i ICloneable

Przedstawię przykładowe zastosowanie wzorca prototyp z wykorzystaniem i omówieniem metody MemberwiseClone() i inerfejsu ICloneable.

Wzorzec prototyp krótko mówiąc służy do  tworzenia obiektów na podstawie innych obiektów wzorcowych poprzez kopiowanie ich. Taki skopiowany obiekt może dziedziczyć wszystkie właściwości jak i metody lub może dziedziczyć tylko niektóre właściwości. Najłatwiej zrozumieć ten wzorzec na przykładzie zapisywania stanu obiektu.


Możemy sobie wyobrazić grę Sokoban. Gra składa się z konkretnego poziomu na którym znajduje się gracz, położenia gracza i położenia skrzyń.

Wyobraźmy sobie teraz, że chcemy umożliwić graczowi zapisanie stanu gry podczas gry. Najprościej będzie zapisać aktualny stan gry wraz ze wszystkimi jego właściwościami. Poniżej klasa abstrakcyjna, która mogła by reprezentować ten model.


public abstract class StanGry : ICloneable
 {
     public string NazwaGracza {get; set;}

     public int Poziom { get; set; }

     public Point PolozenieGracza { get; set; }

     public Point PolozenieSkrzyni { get; set; }

     public abstract object Clone(); }
 }

Zastosowany tutaj interfejs ICloneable nakazuje implementacje metody Clone(). Interfejs ten nie jest wymagany jednak zastosowanie go nie jest błędem a trochę poprawia czytelność kodu (o tym dlaczego takie podejście może mieć też wady piszę niżej)

Następnie rozpoczynamy grę i tworzymy implementację stanu gry.


 public class Gra : StanGry
 {
     public override object Clone()
     {
         return this.MemberwiseClone();
     }

Metoda MemberwiseClone() zwraca kopie obiektu wraz z kopią wszystkich atrybutów jak i metod. W momencie w, którym będziemy chcieli zapisać stan gry możemy wykonać metodę Clone() i uzyskamy stan gry w danym momencie.

Poniżej prezentuje przykładowe działanie zapisywania stanu gry.


public class Sokoban
    {
       public void Graj()
       {
           var stanGry = new Gra
           {
               Poziom = 1,
               PolozenieGracza = new Point(0,0),
               PolozenieSkrzyni = new Point(10,3)
           };

           Console.WriteLine("Zaczynamy grę");

           Console.WriteLine("Poziom {0}, PolozenieGracza {1}, PolozenieSkrzyn {2} ",
               stanGry.Poziom,
               stanGry.PolozenieGracza,
               stanGry.PolozenieSkrzyni);

           stanGry.PolozenieGracza = new Point(12, 3);
           stanGry.PolozenieSkrzyni = new Point(10, 1);

           Console.WriteLine("Gra się toczy, wartości położenia się zmieniają");
           Console.WriteLine("Poziom {0}, PolozenieGracza {1}, PolozenieSkrzyn {2} ",
               stanGry.Poziom,
               stanGry.PolozenieGracza,
               stanGry.PolozenieSkrzyni);

           var zapisanyStanGry = (StanGry)stanGry.Clone();

           Console.WriteLine("Zapisujemy stan gry");

           stanGry.PolozenieGracza = new Point(2, 3);
           stanGry.PolozenieSkrzyni = new Point(5, 3);

           Console.WriteLine("Gra toczy się dalej, wartości położenia się zmieniają");

           Console.WriteLine("Poziom {0}, PolozenieGracza {1}, PolozenieSkrzyn {2} ",
               stanGry.Poziom,
               stanGry.PolozenieGracza,
               stanGry.PolozenieSkrzyni);

           Console.WriteLine("Zapisany stan gry");

           Console.WriteLine("Poziom {0}, PolozenieGracza {1}, PolozenieSkrzyn {2} ",
               zapisanyStanGry.Poziom,
               zapisanyStanGry.PolozenieGracza,
               zapisanyStanGry.PolozenieSkrzyni);
       }

    }

Wynik działania programu pokazuje, że skopiowaliśmy obiekt.

prototyp_2
Do klonowania obiektów użyłem metody MemberwiseClone(), która dostępna jest dla każdego obiektu.

Jedyną rzeczą o jakiej należy pamiętać to, że metoda  MemberwiseClone() robi tzw. płytką kopię. Klonuje tylko typy pierwotne inaczej mówiąc nie referencyjne. Dla każdego typu referencyjnego zostanie utworzona kopia adresu do tego obiektu. A nie nowy obiekt.

Przykład kodu, który pokazuje na czym polega płytka kopia poniżej.

Wprowadzimy zmianę w klasie abstrakcyjnej StanGry. Wejdziemy na poziom drugi gry Sokoban gdzie będziemy mieli na planszy 2 skrzynie. Modyfikujemy linijkę 9. Położenie skrzyń będziemy zapisywać w tablicy. Tablice w C# są typami referencyjnymi.


public abstract class StanGry : ICloneable
 {
     public string NazwaGracza {get; set;}

     public int Poziom { get; set; }

     public Point PolozenieGracza { get; set; }

     public Point[] PolozenieSkrzyn { get; set; }

     public abstract object Clone(); }
 }

Po wykonaniu programu widzimy następujący wynik:

prototyp_1

Po skopiowaniu obiektu do zapisania stanu gry wykonujemy kolejne ruchy. Gdy sprawdzamy co się znajduje w skopiowanym obiekcie po wykonaniu ruchów widzimy, że położenie skrzyń jest takie samo jak aktualne położenie skrzyń. Czyli zmiana wartości położenia skrzyń w aktualnym stanie gry spowodował zmianę stanu położenia skrzyń w zapisanym stanie gry.

Taka właśnie jest metoda MemberwiseClone(). Wykonuje kopię tylko typów nie referencyjnych. Należy o tym pamiętać.

Zastosowany tutaj interfejs ICloneable nakazuje implementacje metody Clone(). Interfejs ten informuje jasno, że obiekt może być skopiowany.

Jednak interfejs ten ma pewną wadę, którą objawia się tym, że gdy widzimy obiekt, który implementuje ten interfejs to nigdy nie wiemy czy jest to płytka kopia czy głęboka kopia (czyli kopia całego obiektu wraz z utworzeniem nowych obiektów dla typów referencyjnych). Nie możemy tego w żaden sposób tego stwierdzić. Dlatego stosowanie tego interfejsu jest dość niebezpieczne i mylące.