Co jest zasadnym powodem, by przekabacić metodę
Piotr Zieliński na swoim blogu rozważał zasadność redefiniowania przez klasy dziedziczące metod z klas dziedziczonych (przy pomocy modyfikatora new), warto zapoznać się z tym wpisem przed kontynuowaniem lektury niniejszego tekstu. Na zakończenie Piotr poprosił o podanie innych, od przestawionych przez niego, powodów, na zasadność użycia modyfikatora new. Początkowo chciałem uczynić to w komentarzu do Jego wpisu, ale ubogość formy, jaki ma system komentarzy na blogach skutecznie mnie zniechęciła. Postanowiłem, że zrobię to na swoim blogu, a w komentarzach jedynie dam odnośnik.
Kiedy więc – tytułowe – przekabacanie metody się opłaca ;). Załóżmy, że mamy klasę, z bogatą funkcjonalnością, która wśród wielu metod ma jedną, dość istotną z punktu widzenia wpływu na tęże funkcjonalność, ale niestety – niewirtualną. A my chcielibyśmy wykorzystać tę klasę jako bazową, w celu uzupełnienia kilkoma dodatkowymi funkcjonalnościami, z jednoczesnym zachowaniem polimorfizmu (czyli wywoływana metoda ma zależeć od instancji obiektu, a nie jego typu – zatem wirtualność wspomnianej metody zaczyna mieć znaczenie). Napisanie takiego mechanizmu od podstaw nie jest opłacalne, chcemy naprawdę dodać kilka drobnych, acz istotnych mechanizmów, ale nie kosztem pisania całego mechanizmu. Nie bardzo też możemy/chcemy modyfikować źródło owej bazowej klasy (choćby ze względów braku źródeł, licencji, czy też świadomości, że nie jest ona niezależnym bytem i już istnieją inni jej „dziedzice”, którym możemy pokrzyżować szyki). Co w takim razie uczynić?
Jednym z rozwiązań jest zastosowanie wzorca Adapter, ale perspektywa delegowania bezmiaru metod klasy bazowej na klasę adaptera nie budzi naszego entuzjazmu. My chcielibyśmy tylko „przefarbować” metodę na wirtualną. I tu z pomocą przychodzi nam właśnie modyfikator new.
Przyjrzyjmy się klasie bazowej (na potrzeby czytelności przykładu, odpowiednio uproszczonej).
class Introduce { // tu masa kodu, który robi przepiękne rzeczy public string Method() { return "jestem ZWYCZAJNA"; } // tu reszta masy kodu, który robi przepiękne rzeczy }
A teraz stwórzmy jej potomka i zwirtualizujmy metodę Method().
class Reintroduce : Introduce { public new virtual string Method() { return "już nie " + base.Method() + ", tylko wirtualna"; } }
Właśnie przekabaciliśmy metodę Method() na naszą stronę ;).
Oczywiście można pominąć modyfikator new i to także zadziała, ale wówczas kompilator nie będzie pewien naszych intencji i będzie nas nękał ostrzeżeniem (w końcu po to jest modyfikator new ;), aby nas nie nękał).
Teraz możemy potraktować klasę Reintroduce, jako naszą klasę wyjściową, która umożliwi nam korzystanie z polimorfizmu. To do zmiennej jej typu będą przypisywane instancje jej potomków. Pokażmy prosty przykład jednego z nich.
class Descendant : Reintroduce { public override string Method() { return "wywołuję tę, co twierdzi: \"" + base.Method() + "\" i dodaję coś od siebie"; } }
Od teraz możemy już programować następująco:
Reintroduce R; R = new Reintroduce(); Console.WriteLine("Reintroduce.Method() => " + R.Method() + ".\n"); R = new Descendant(); Console.WriteLine("Descendant.Method() => " + R.Method() + ".\n");
i korzystać z uroków polimorfizmu oraz wszystkich funkcjonalności klasy Introduce.
Dla równowagi warto jeszcze podać przykład pokazujący, co działoby się w przypadku użycia klasy Introduce.
Introduce I; I = new Introduce(); Console.WriteLine("Introduce.Method() => " + I.Method() + ".\n"); I = new Reintroduce(); Console.WriteLine("Reintroduce.Method() => " + I.Method() + ".\n"); I = new Descendant(); Console.WriteLine("Descendant.Method() => " + I.Method() + ".\n");
Czyli dokładnie to, co klarował Piotr na swoim blogu (tj. to typ zmiennej determinuje wybór używanej metody, a nie jej instancja).