Podstawowe zasady projektowania obiektowego
Transkrypt
Podstawowe zasady projektowania obiektowego
Śliwioski Paweł, 30.04.2009 Podstawowe zasady projektowania obiektowego Wstęp Programowanie zorientowane obiektowo dominuje w świecie wytwarzania oprogramowania od ponad dwóch dekad, jednak wiele projektów obiektowych nadal nie jest do kooca zgodnych z jego ideą. Wielu programistów wykorzystuje języki obiektowe takie jak Java czy C# do tworzenia kodu proceduralnego, który jest trudny w konserwacji i podatny na błędy. W dalszej części przedstawię zasady dzięki, którym będziemy mogli powiedzied, że nasz projekt jest w pełni obiektowy. Zasada poj edyncze j odpowi edzi alności Zasada pojedynczej odpowiedzialności Klasa powinna mied wyłącznie jeden obszar odpowiedzialności. Obszar odpowiedzialności klasy jest to zakres - zbliżonych funkcjonalnie – działao, które może wykonywad obiekt będący jej instancją. Przykładowo na poniższym rysunku klasa Kolo posiada dwa obszary odpowiedzialności – rysowanie oraz wykonywanie obliczeo matematycznych. Z klasy tej korzysta Aplikacja graficzna oraz Moduł matematyczny. Taka sytuacja powoduje poważny problem. Jeśli Aplikacja graficzna wymusi zmianę klasy Kolo, musimy uwzględnid tą zmianę w module matematycznym. W najlepszym przypadku będziemy musieli ponownie go skompilowad i przetestowad, w najgorszym zmuszeni będziemy do modyfikacji jego kodu. W celu rozwiązania tego problemu, należy podzielid obszary odpowiedzialności między dwie odrębne klasy. Przykładowe rozwiązanie prezentuje poniższy diagram. Przypisanie obszarów odpowiedzialności do odrębnych klas sprawiło, że zmiany w klasie Kolo powodowane zmianą wymagao Aplikacji graficznej - nie mają żadnego wpływu na funkcjonowanie Modułu matematycznego. 1 Zasada otwarte-zamknięta Zasada otwarte-zamknięte Składniki oprogramowania (klasy, moduły, funkcje itp.) powinny byd otwarte na rozbudowę, ale zamknięte dla modyfikacji 1 . Składnik oprogramowania zgodny z tą zasadą, powinien udostępniad mechanizm swojej rozbudowy, nie ingerujący w jego istniejący kod, a więc w celu jego rozbudowy, dodajemy dodatkowe struktury obiektowe (tj. klasy, interfejsy, łączące je relacje), a nie modyfikujemy struktur już istniejących. Zgodnośd z tą zasadą można osiągnąd stosując odpowiednie wzorce projektowe tj. Strategy lub Template Method. Przykład klasy Importer, niezgodnej z zasadą otwarte-zamknięte, przedstawiam na poniższym diagramie. Gdybyśmy chcieli rozszerzyd ten model o możliwośd importu danych typu ImportYData, musielibyśmy obsłużyd go w klasie Importer, co jest naruszeniem zasady otwarte-zamknięte. Sposób eliminacji tego problemu, z wykorzystaniem wzorca Strategy, prezentuje poniższy diagram klas: W celu rozbudowy tej struktury o dodatkowe typy importowanych danych, dodajemy kolejne klasy implementujące interfejs ImportInterface, natomiast nie musimy wprowadzad żadnych zmian do istniejącego kodu. Zachowanie zgodności z zasadą otwarte-zamknięte jest dośd kosztowne. Tworzenie niezbędnych abstrakcji wymaga dodatkowej pracy programisty, a same klasy zwiększają złożonośd projektu, dlatego zaleca się stosowanie jej dla tych fragmentów systemu, które są najbardziej podatne na zmiany. Zasada podstawiania Li skov Zasada podstawiania Liskov Musi istnied możliwośd zastępowania typów bazowych ich podtypami 2. Naruszenie tej zasady stwarza jeden podstawowy problem. W każdym miejscu programu, w którym oczekujemy typu B, będącego klasą bazową typu P, użycie typu P spowoduje nieprawidłowe 1 2 Robert C. Martin i Micah Martin, Agile Principles, Pa tterns, and Pra cti ces In C#, Prentice Hall, 2007, s. 122. Robert C. Martin i Micah Martin, Agile Principles, Pa tterns, and Pra cti ces In C#, Prentice Hall, 2007, s. 136. 2 działanie programu. Co gorsza, w celu eliminacji tego problemu, często stosuje się konstrukcje postaci: void F(B b) { if(b is P) (b as P).Wykonaj(); } Powoduje ona degradację kodu i w oczywisty sposób narusza zasadę Otwarte-zamknięte (stworzenie nowego typu potomnego klasy B, spowoduje koniecznośd uzupełnienia funkcji F o dodatkowy warunek). W celu eliminacji tego rodzaju problemów, należy w klasie bazowej stosowad odpowiednią metodę wirtualną i nadpisywad ją w klasach potomnych. Podejście takie umożliwia podstawienie typu P w miejsce B, co eliminuje koniecznośd sprawdzania rzeczywistego typu obiektu, który został przekazany funkcji. Zasada odwracana zal eżności Zasada odwracania zależności Moduły wysokiego poziomu nie powinny zależed od modułów niskopoziomowych. Obie grupy modułów powinny zależed od abstrakcji.3 W większości aplikacji to moduły wysokiego poziomu opisują strategie działania sytemu. Dlatego zgodnie z logiką moduły te nie powinny zależed od szczegółowych mechanizmów warstw niskopoziomowych. Przeanalizujmy poniższy przykład 4: Klasa Przycisk reprezentuje wysokopoziomową strategię aktywacji / dezaktywacji przycisku w zależności od czynników zewnętrznych. Natomiast klasa Czajnik zawiera niskopoziomowe mechanizmy obsługi działania czajnika elektrycznego. Podejście zgodne z powyższym diagramem sprawia, że zmiany w klasie Czajnik wymuszą modyfikację klasy Przycisk, dodatkowo w takim modelu obiekty klasy Przycisk mogę sterowad wyłącznie obiektami klasy Czajnik. W celu eliminacji tych problemów należy odwrócid zależnośd, tak jak przedstawiono to na poniższym diagramie: 3 4 Robert C. Martin i Micah Martin, Agile Principles, Pa tterns, and Pra cti ces In C#, Prentice Hall, 2007, s. 154. Przykład zaczerpnięty z książki Agile Principles, Patterns, and Pra cti ces In C#, Pr entice Hall, 2007 3 W tej postaci obiekty klasy Przycisk zależą wyłącznie od abstrakcji (SwitchableDevice) i mogą sterowad dowolnym obiektem klasy implementującej interfejs SwitchableDevice np. obiektem klasy Telewizor. 4