Apr 04 2007
O abstrakcji klas i interfejsów
Filed under Java,Ogólne,PHP by Łukasz Dywicki
Od jakiegoś czasu na forum.php.pl spotykam się z różnymi zdaniami na temat interfejsów i klas abstrakcyjnych. Argumenty, które czasami się trafiają są chybione.
Zacznijmy od tego, że trzeba potrafić rozróżnić interfejs od klasy abstrakcyjnej, to nie to samo!
Interfejs jest najwyższym poziomem abstrakcji, który definiuje nowy, wolny od implementacji typ. Bez jakiejkolwiek linii kodu, tylko sygnatury metod publicznych.
Klasa abstrakcyjna jest już początkiem konkretnej implementacji, zawiera kod ogólny i wymusza w klasach dziedziczących dorzucenie konkretnych metod, które są specyficzne, inne, różne. Mogą trafić się takie przypadki, że klasa abstrakcyjna zawiera 5 metod, a jej pochodne tylko jedną. Czy jest to uzasadnione? Oczywiście. Ta jedna metoda determinuje nowy typ, który jest jawną specjalizacją.
Warto pamiętać, że w chwili gdy mamy klasę abstrakcyjną i kilka jej pochodnych dorzucenie interfejsu jest polisą na życie. Spójrzmy, na praktykę – co się powinno dziać w chwili, gdy pojawiają się kolejne pochodne oraz klasa abstrakcyjna rozrasta się do niebagatelnych rozmiarów? Wyodrębniamy wspólny interfejs (patrz extract interface) i kolejne klasy abstrakcyjne.
Bez problemu dorzucamy kolejny typ i lecimy bez przestojów. W chwili gdy uzależnimy się od klasy abstrakcyjnej z jakimkolwiek kodem z biegiem czasu możemy mieć do czynienia z klasami, które zawierają po 10, 20 i więcej metod. Nawigacja po tak rozbudowanym typie jest uciążliwa a zrozumienie kodu wymaga przebrnięcia przez stos metod, które tworzą swoisty labirynt.
Rozsądne użycie interfejsów i klas pozwala nam tworzyć złożone ale przejrzyste struktury. Separacja abstrakcji od implementacji określana mianem wzorca projektowego, nazwa mostem (ang. bridge).
Budowa złożonej aplikacji bez użycia interfejsów jest posunięciem wysoce nierozsądnym, ponieważ to właśnie dzięki nim, nawet w językach kompilowanych ze statycznym typowaniem jesteśmy w stanie zyskać nadzwyczajnie elastyczny kod. W aplikacji, która jest rozwijana od dłuższego czasu (powiedzmy, ponad pół roku) to właśnie interfejsy są miejscami zawierającymi definicję poszczególnych warstw. To one powinny być trzonem każdej aplikacji (pomijając warstwę obiektów domenowych, gdzie ich rola nie musi być dominująca). Nawet jeśli koncepcja implementacji obróci się o 180 stopni to nasz interfejs, będący swoistą fasadą, ukryje te zmiany przed osobami, których one nie dotyczą bądź nie interesują.
Dorzucę tutaj przykład pewnego mechanizmu, który jakiś czas temu projektowałem i implementowałem. Jest to uproszczony schemat. Singletonu chciałem uniknąć, ale przesłanki ku jego zastosowaniu były na tyle duże, że pozostał. Duże I na początku nazwy oznacza interfejs, w nawiasach, dużymi literami wzorce.
(zdarzenie) (Mediator) (odbiorca) ProgressEvent ---> IProgressMonitor <--- IProgressListener (generuj zdarzenie) ^ ^ IDownloader ----------+ | IMerger | (Composite) | (Singleton) IProgressMonitorGroup < - - - SynchronizeManager ISynchronizeRequest ^ | (Strategy) AbstractSynchronizeRequest +--------------+--------------+ | | | | TableSynchronizeRequest | ClassSynchronizeRequest ModuleSynchronizeRequest
Panowie, jak byście zaimplementowali te mechanizmy bez interfejsów? Jedną, wielką protezą?
Splatch, a co z takim rozwiązaniem ? http://playme.2adv.pl/?f=inter.php
Już kiedyś chwilke o tym rozmawialiśmy… Nawet błąd przez php jest zwracany w sposób indentyczny. Tak jak by php traktowało interfejs jak klase abstrakcyjną :| Coś w tym chyba musi być bo już spotkałem się (AFAIR PHP5: power programming, ale ręki nie dam sobie uciąć) z takim podejściem do tematu.
Zaczynam się zastanawiać czy czasami w PHP reprezentacja interfejsów za pomocą _takich_ klas abstrakcyjnych nie będzie w pewnych przypadkach wygodniejsza ?
…kawyyy :)
Interfejs nie może pełnić roli klasy abstrakcyjnej a tym bardziej klasa abstrakcyjna roli interfejsu. To nie to samo! Być może język sprowadza to do podobnej postaci, ale nie zmienia to faktu iż zamienne stosowanie jednego i drugiego to pomyłka. Interfejs to definicja, klasa abstrakcyjna to początek implementacji i w taki sposób powinno to być wykorzystywane. Nie inny.
@x^k – widzisz różnicę http://phpfi.com/223327?
Splatch jasne :) ja wiem o tym, ale to nie zmienia faktu, że jednak ktoś tak zrobił m.in. w tej księdze. I to mnie meczy… Czyżby aż tak wielki błąd popełnili ? Postaram się odnaleźć e-booka, żeby nie pozostać gołosłownym :)
PS. Tytuł jednak pomyliłem… To chodziło o tą książkę: http://helion.pl/ksiazki/php5ob.htm
W kodzie na phpfi.com jest błąd w nazwie klasy po której dziedziczy Mysql ;) ale ogólnie dzięki za tą notkę, rozjaśniło mi bardziej sprawę różnicy klas abstrakcyjnych od interfejsów :)
Niewspominając juz o tym, ze jedna klasa moze wygodnie implementowac wiele interfejsow, a chcac zrealizowac to samo na klasach abstrakcyjnych trzeba tworzyc wielopoziomowe dziedziczenie.
Pozdro!
Bardzo dobry tekst. Ja od pewnego czasu praktycznie nie tworzę kodu bez interfejsów. Szczególną ich przydatność znalazłem przy pisaniu silnika gry w C++. Design oparty na interfejsach jest tu po prostu magiczny. Raz, pozwala zupełnie oddzielić silnik od platformy – najważniejsze podsystemy (grafika, VFS, dźwięk) implementuje się za interfejsem i nic, poza samą implementacją podsystemu (i punktem ich tworzenia) nie wie o tej implementacji. Dwa, można oddzielić kod silnika od kodu stawianego na niej gry – ponieważ silnik stanowi już zamknięte API, można w ogóle nie przejmować się systemem.
Sposób, w jaki takie podejście czyści i porządkuje kod jest wręcz zaskakujący. A debugowanie błędów to sama przyjemność.
Język C++ nie robi akurat żadnej różnicy semantycznej między klasą abstrakcyjną a interfejsem – to drugie jest tylko umownym terminem na czysto wirtualną klasę zawierającą jedynie funkcje publiczne. Niemniej różnice w użyciu są ogromne :)
Co ciekawe, interfejsy są dostępne nie tylko w językach tak wysokiego poziomu jak C++, Java, PHP. W jakimś artykule na GameDev.NET czytałem kiedyś stwierdzenie – to, że język nie jest Object-Oriented nie powoduje, że kod nie może być Object-Oriented. Osobiście widziałem próby fabrykowania funkcjonalności interfejsów w języku C w kodzie źródłowym gry Quake II – strukturka zawierająca tylko wskaźniki na funkcje (co w praktyce podrabiało mechanizm funkcji wirtualnych) była tworzona w jednym miejscu programu, a następnie przekazywana do innego (głównie między DLL’ami a samym silnikiem).
Pozdrawiam!
To ja sie przyczepie …
Nie wymieniles kilku istotnych roli obu konstruktow. Jezeli chodzi o klasy abstrakcyjne:
Moga zachowywac sie zupelnie tak samo jak interfejsy – definiujac wylacznie metody abstrakcyjne – moga takze definiowac zwykle metody plubliczne, ktore to beda wykonywaly jakas logike operujac na implementacjach metod abstrakcyjnych. Sytuacja: istnieje sobie klasa abstrakcyjna, definiujaca dwie metody: pierwsza metoda jest oznaczona jako “abstract” co powoduje, ze implementacja klasy bedzie pociagala za soba koniecznosc implementacji tejze metody. Druga metoda, publiczna, bedzie wykonywana bezposrednio przez mechanizmy naszej aplikacji/systemu/biblioteki. Ona to bedzie decydowala kiedy i w jaki sposob wykonac metode abstrakcyjna (jakie parametry do niej przekazac) – mozna dlugo wymieniac zastosowania i powody uzycia… Poslugiwanie sie tylko interfejsami niczego nie zalatwia, ba, czasem moze bardzo komplikowac zycie – interfejs moze byc zaimplementowany przez dowolna klase, mozna sie pogubic z separacja odpowiedzialnosci … Ale co ja tam wiem ;)
[…] właściwie są klasa abstrakcyjna i interfejs. Pozwolę sobie tutaj zacytować fragment podobnego artykułu z blogu Splatch’a. (…) trzeba potrafić rozróżnić interfejs od klasy abstrakcyjnej, […]