poniedziałek, 5 maja 2014

Publiczne metody w abstrakcie = interfejs istnieje

a zaczęło się to tak...

Jak każdy (a przynajmniej mam taką nadzieję) zespół ludzi pracujący nad tym samym kodem, i w naszym projekcie istnieje dokument ze standardami kodowania. Co jakiś czas wprowadzamy drobne modyfikacje, bo refaktoryzować można (i powinno się) nie tylko kod :)

Dzisiaj ten właśnie dokument przeglądał jeden z moich kolegów i natknął się na jedną z moich notatek/uwag w tym dokumencie, którą odrobinę przeredagowaną (niestety użyłem skrótów myślowych pisząc ją :) zamieszczam poniżej:
Jeżeli posiadasz klasę abstrakcyjną z metodą publiczną, która jest rozszerzana przez kilka klas nie implementujących tego samego interfejsu, to wiedz, że coś się dzieje.

Dobra, samo stwierdzenie może nie być zbyt oczywiste, ale wierzę, że po krótkich wyjaśnieniach dojdziecie nie tylko do wniosku, że ma to sens, ale co więcej, że sami już tą zasadę w kodzie stosujecie.
Jakby na to nie patrzeć, jest całkiem naturalna :)

skąd ten abstrakt

Klasę abstrakcyjną zazwyczaj (zawsze?) tworzymy, bo zauważamy, że w co najmniej dwóch klasach występuje kawałek kodu, który jest taki sam i który, ze względu na DRY, warto byłoby gdzieś wynieść i uniknąć duplikacji. Oczywiście, niekiedy o klasie abstrakcyjnej wiemy jeszcze nim zaczniemy cokolwiek pisać, bo stanowi część rozwiązania, które wybieramy (np. wykorzystanie Template Method), jednak i w takim momencie to konkretne klasy są dla nas interesujące, a nie abstrakcja. I jakby na to nie patrzeć, w takim wypadku również jest ona wynikiem zauważenia pewnych powtarzalności w kodzie/działaniu obiektów klas potomnych.

Idziemy dalej. Dwie klasy mogą:
  • Leżeć w osobnych miejscach aplikacji tak, że jedna nie wie o istnieniu drugiej.
  • Należeć do tego samego modułu, czyli być w jakiś sposób powiązane.

nieznajomi

Zacznijmy od pierwszej sytuacji, kiedy dwie klasy leżą w modułach całkowicie od siebie niezależnych. Jeżeli mamy część wspólną i decydujemy się na stworzenie klasy abstrakcyjnej z choćby jedną metodą publiczną, to czy rozsądnym byłoby, aby te klasy imeplementowały jakiś wspólny interfejs? Raczej wątpię. W końcu każdy interfejs to kolejna zależność. Tylko, że... klasa abstrakcyjna to też zależność? Więc... chyba coś tutaj jest nie tak?

I właśnie z powodu, dla którego nie powinno się tworzyć wspólnego interfejsu dla takich klas, wyciąganie części wspólnej do abstrakcyjnych klas nie jest zbyt dobrym pomysłem. Niestety są to zależności dość mocne i jeżeli dwie (bądź więcej) klas nimi zwiążemy to dajemy również sygnał, że te klasy należą do jednej grupy. Co prawdą przecież nie jest, one jedynie używają tego samego kawałka kodu... No właśnie :) W takich wypadkach dobrym pomysłem jest wydelegowanie tego kodu do osobnej klasy. Dzięki temu moduły nadal są odseparowane od siebie, a jeżeli wykorzystamy interfejsy to nawet nie muszą wiedzieć o istenieniu konkretnej klasy, do której kod został wydelegowany :)

czy my się skądś znamy?

Następny przystanek - klasy leżące w tej samej przestrzeni.

To zaczynamy od początku - mamy dwie klasy, mają one część wspólną tzn., że zachowanie jest co najmniej podobne. Bardzo prawdopodobne, że te klasy są wykorzystywane w pewnym miejscu zamiennie, bo albo zdecydujemy się na jedną albo na drugą. Jakiś powód przecież podobnie działającej metody publicznej musi być? Jeżeli są wykorzystywane gdzieś, zamiennie, to istnieje szansa, że dana metoda (wykorzystująca instancje tych klas) ma parametr, którym jest klasa abstrakcyjna właśnie. Tylko, że dlaczego klasa abstrakcyjna? Przecież nam zależy na funkcjonalności, na kontrakcie. Czy to więc nie czas na interfejs?

czy klasa abstrakcyjna powinna implementować interfejs?

Wielokrotnie spotkałem się (ba, wielokrotnie sam tak robiłem/robię) z tym, że gdy mamy strukturę klas taką, że istnieją:
  • Interfejs Inter.
  • Klasy implementujące interfejs Inter.
  • Klasa abstrakcyjna Abst, która zawiera część wspólną klas z punktu drugiego.
  • Klasa, której niektóre metody deklarują wykorzystanie parametrów będących typem Inter.
To programista często decyduje się na to, żeby klasa Abst implementowała interfejs Inter, natomiast wszystkie klasy z punktu drugiego "jedynie" rozszerzają klasę abstrakcyjną. Czyli pośrednio również implementują wspomniany innterfejs. I choć taki kod nie ma większego wpływu na działanie komponentu, to osobiście uważam, że lepiej jednak pozostawić klasę abstrakcyjną samą sobie, a fragment "implements z Inter" dodać do każdej klasy.
Ja wiem, że to kilka(naście) linijek kodu więcej, ale przy autouzupełnianiu w dzisiejszych IDE, to raczej nie stanowi większego problemu. Poza tym, wpływa pozytywnie na czytelność kodu, ponieważ patrząc jedynie na kod klasy od razu widzimy jakie kontrakty musi ona spełniać, nie musimy dodatkowo skakać do abstrakcyjnej klasy, którą rozszerza.

Ważne jest również to, że tak naprawdę klasa abstrakcyjna implementująca interfejs to taki programistyczny oksymoron (z punktu widzenia projektu). Bo albo coś implementuje interfejs i spełnia jego kontrakt, albo jest klasą abstrakcyjną, której instancji nie jesteśmy w stanie utworzyć :)

i jeszcze na koniec

Chciałbym jeszcze dodać na koniec, że nie twierdzę, że powyższe jest regułą i możliwe, że są wyjątki. Niemniej jednak warto dwa razy się zastanowić, gdy zobaczycie klasę abstrakcyjną, której klasy potomne nie implementują wspólnego interfejsu. Może okazać się, że uda Wam się zwiększyć spójność Waszego kodu.

PS. Zastanówcie sie czy podobne implikacje nie zachodzą w sytuacjach, gdy klasa abstrakcyjna nie zawiera metod publicznych :)