GRASP

General Responsibility Assignment Software Patterns (GRASP) to zbiór 9 zasad określających, jaką odpowiedzialność powinno się przypisywać określonym obiektom i klasom w systemie. Wszystkie te zasady odpowiadają na część problemów z oprogramowaniem, które są wspólne dla prawie każdego projektu. Techniki te nie zostały wymyślone w celu stworzenia nowych sposobów pracy, a jedynie w celu lepszego udokumentowania oraz standaryzacji starych, wypróbowanych i przetestowanych zasad programowania. Jest więc to kolejny zbiór zasad, który może nam pomóc w stworzeniu projektu łatwego w utrzymaniu i rozwoju.

Te zasady to:

  • Controller – opisuje jak powinna wyglądać komunikacja interfejsu użytkownika z logiką biznesową.
  • Creator – opisuje kiedy klasa powinna tworzyć obiekt innej klasy.
  • Indirection – opisuje jak zorganizować komunikację między komponentami, aby ich mocno nie wiązać.
  • Information Expert – opisuje jak ustalać zakres odpowiedzialności obiektu.
  • High Cohesion – opisuje jak ograniczać zakres odpowiedzialności obiektu.
  • Low Coupling – opisuje jak ograniczyć zakres zmian w aplikacji w momencie zmiany fragmentu kodu.
  • Polymorphism – opisuje co zrobić, gdy zachowania różnią się w zależności od typu.
  • Protected Variations – opisuje jak projektować klasy, by ich zmiana nie destabilizowała innych klas.
  • Pure Fabrication – opisuje komu oddelegować zadanie, jeśli nie można zidentyfikować do kogo należy podany zbiór operacji.

Controller

Jeśli nasza aplikacja zawiera interfejs użytkownika, powinniśmy stworzyć klasę, która będzie zajmować się obsługą zdarzeń z tego interfejsu. Ta klasa będzie pełnić funkcję kontrolera (controller). Powinna on znajdować się w osobnej warstwie – warstwie pomiędzy interfejsem użytkownika, a logiką biznesową. Sama w sobie nie powinna zawierać żadnej logiki biznesowej, a jedynie pełnić funkcję koordynatora, czyli wywołać odpowiednią metodę z warstwy logiki biznesowej.

Przykład:

Załóżmy, że na interfejsie użytkownika mamy formularz z możliwością dodania nowego zamówienia. Obsługa tego zdarzenia nie powinna odbywać się po stronie tego interfejsu. W związku z tym w momencie wystąpienia zdarzenia (np. naciśnięcie przycisku „Dodaj” lub „Zamów”) wszystkie niezbędne informacje powinny zostać przekazane do kontrolera. Następnie kontroler podejmuje decyzję co należy dalej zrobić.

Jeden kontroler może obsługiwać kilka zdarzeń – np. możemy mieć OrderController, który będzie obsługiwał zdarzenia dodawania, edycji i usunięcia zamówienia.

Creator

Chyba w każdej aplikacji tworzenie obiektów jest jedną z najczęstszych operacji. Tworzymy bardzo dużo obiektów, ale czy robimy to w odpowiednim miejscu? Zazwyczaj nie do końca wiemy gdzie dany obiekt powinien zostać stworzony – czy powinniśmy to zrobić w danej klasie, czy powinniśmy mieć osobną klasę, która zrobi to za nas.

Ogólnie mamy kilka zasad, kiedy klasa A powinna być odpowiedzialna za stworzenie obiektu klasy B. Dzieje się tak, jeśli spełniony jest co najmniej jeden z następujących warunków:

  • Obiekt klasy A zawiera lub agreguje obiekty klasy B.
  • Obiekt klasy A zapisuje obiekty klasy B.
  • Obiekt klasy A ściśle wykorzystuje obiekty klasy B.
  • Obiekt klasy A zawiera informacje potrzebne do stworzenia obiektu klasy B.

W dużym uproszczeniu ta zasada mówi, kiedy powinniśmy zastosować wzorzec fabryki (factory pattern).

Indirection

Jeśli chcemy uniknąć bezpośredniego powiązania między komponentami, powinniśmy stworzyć klasę pośrednią, która będzie odpowiedzialna za komunikację między nimi.

Jako przykład możemy tutaj dać logikę biznesową i bazę danych. W logice biznesowej potrzebujemy wykorzystać dane, które są zapisane w bazie danych. Jednakże logikę biznesową nie interesuje to, jak te dane z tej bazy danych wyciągnąć, ani też w jakiej bazie są przechowywane. Co za tym idzie nie chcemy wiązać logiki biznesowej z bazą danych. W tej sytuacji możemy stworzyć klasę pośrednią (np. repozytorium), która będzie pełnić rolę łącznika pomiędzy logiką biznesową, a bazą danych.

Information Expert

Czasem zastanawiamy się gdzie powinniśmy umieścić daną funkcjonalność (w jakiej klasie powinna się ona znajdować). Tu z pomocą może nam przyjść reguła Information Expert. Mówi ona o tym, że odpowiedzialność za daną funkcjonalność powinna znajdować się w obiekcie, który ma najwięcej informacji związanych z daną funkcjonalnością.

Załóżmy, że mamy zamówienie, które składa się z kilku pozycji. Na każdej z pozycji znajduje się ilość zamawianego towaru wraz z jego ceną (za jedną sztukę). Jeśli potrzebowalibyśmy wyliczyć sumę dla całego zamówienia, to taka funkcjonalność powinna znajdować się w obiekcie zamówienia, ale już wyliczanie sumy dla poszczególnych pozycji powinno znajdować się w obiekcie pozycji zamówienia.

High Cohesion

Chyba wszyscy chcemy tworzyć klasy, które mają jasny cel, są zrozumiałe i łatwe w utrzymaniu. Nasze życie jest wtedy dużo prostsze. Możemy to osiągnąć dzięki zachowaniu wysokiej spójności (high cohesion). Ta zasada mówi o tym, że należy tworzyć na tyle małe klasy, aby wszystkie ich zależności i funkcjonalności były ze sobą ściśle powiązane i wysoce skoncentrowane.

W praktyce oznacza to, że jeśli nasza klasa ma jakąś zależność, powinna ona być wykorzystywana w jak największej liczbie metod (najlepiej we wszystkich). Jeśli jakaś zależność jest wykorzystywana tylko w jednej metodzie (albo kilku, ale nie we wszystkich), to spójność nam spada i powinniśmy zastanowić się nad przeniesieniem danej funkcjonalności do innej klasy.

Low Coupling

Sprzężenie (coupling) jest miarą tego, jak silnie jeden element jest połączony z innymi. Im jest ono większe, tym zmiany w jednej klasie mogą mieć wpływ na większą liczbę klas. To z kolei powoduje, że łatwiej o błędy i więcej czasu należy poświęcić na testy. Utrzymując niski poziom sprzężenia powinniśmy otrzymać kod, który jest łatwy w utrzymaniu i rozwoju.

Oto kilka przykładów, kiedy dwie klasy są ze sobą powiązane:

  • Obiekt jednej klasy dziedziczy po obiekcie drugiej klasy.
  • Obiekt jednej klasy wywołuje metody obiektu drugiej klasy.
  • Obiekt jednej klasy tworzy obiekt drugiej klasy.
  • Obiekt jednej klasy zwraca obiekt drugiej klasy.
  • Obiekt jednej klasy zawiera zmienną (lokalną lub globalną) typu obiektu drugiej klasy.

Co więcej jeśli obiekt jednej klasy jest powiązany z obiektem drugiej klasy, a on jest powiązany z obiektem trzeciej klasy, to obiekt pierwszej klasy jest również powiązany z obiektem trzeciej klasy. Dlatego pisząc kod powinniśmy uważać, czy przypadkiem nie tworzymy klasy, która ma duże sprzężenie.

Polymorphism

Zbiór obiektów może mieć zestaw zachowań wspólnych oraz zestaw zachowań odmiennych. Dla zachowań odmiennych powinniśmy wykorzystać mechanizm polimorfizmu. Podobny efekt można uzyskać korzystając z konstrukcji if-else jednak nie jest to zalecane, ponieważ może doprowadzić do powstania kodu trudnego w zrozumieniu i utrzymaniu.

Jak wygląda polimorfizm?

Załóżmy, że mamy metodę, która duplikuje przekazaną wartość, tzn. jeśli przekażemy do niej ciąg znaków „abc”, to zwróci nam „abcabc”, a jeśli przekażemy do niej liczbę 12, to zwróci 24. Rozwiązaniem bez użycia polimorfizmu byłoby stworzenie dwóch metod: „duplicate_string” (przyjmującą napis jako parametr) oraz „duplicate_int” (przyjmującą liczbę jako parametr). Te dwie metody moglibyśmy połączyć w jedną metodę „duplicate”, która jako parametr przyjmowałaby obiekt. W tej metodzie moglibyśmy dodać dwa warunki if: jeśli obiekt jest napisem, to doklej jego wartość na koniec i go zwróć, a jeśli obiekt jest liczbą, to zwróć podwojoną jej wartość. Oba z tych rozwiązań nie używają polimorfizmu. Rozwiązaniem z użyciem polimorfizmu byłoby np. stworzenie dwóch metod „duplicate” tak, aby każda z nich przyjmowałaby inny typ jako parametr: jedna przyjmowałaby napis, a druga liczbę.

Innym sposobem na wykorzystanie polimorfizmu jest użycie metod wirtualnych (virtual) i ich nadpisanie (override).

Protected Variations

Powinniśmy tak projektować nasze aplikacje, aby łatwo było podmienić jakieś ich komponent na inne. Dlatego ważną rolę ogrywa tutaj tworzenie abstrakcji i opieranie kodu na nich, aby w razie potrzeby można było wymienić jedną implementację na inną. Oczywiście nie powinniśmy tego robić dla wszystkich elementów naszego systemu, a jedynie dla tych, które mogą tego wymagać.

Przykładem może być użycie połączenia z bazą danych – w wersji produkcyjnej chcemy korzystać ze zwykłej bazy danych, a dla testów chcemy użyć bazy danych w pamięci. Dlatego w klasie łączącej się z bazą danych warto zastosować abstrakcję.

Innym przykładem może być stworzenie abstrakcji do wyliczania końcowej ceny zamówienia – czasami to jak ta cena ma być wyliczona może zależeć od różnych czynników. Dlatego przyda nam się polimorfizm lub wzorzec strategii (strategy pattern). Ta reguła jest również mocno powiązana z zasadą otwarty/zamknięty (open/closed principle) z SOLID.

Pure Fabrication

Czasami musimy stworzyć metodę, która nie pasuje do żadnego z naszych istniejących obiektów. Ponadto żadna z wcześniejszych zasad jej nie obejmuje. Ta metoda nie tworzy żadnego obiektu, a jedynie wykonuje jakieś operacje – dlatego nie pasuje do reguły Creator. Nie jest powiązana z żadnym konkretnym obiektem, więc zasada Information Expert odpada. Moglibyśmy tę metodę umieścić bezpośrednio w jakiejś klasie, ale wtedy naruszylibyśmy jedną z reguł High Cohesion albo Low Coupling. W takich sytuacjach powinniśmy stworzyć nową klasę i tam umieścić tę metodę. O tym właśnie jest reguła Pure Fabrication – czasem jest potrzeba stworzenia klasy czysto usługowej.

Słowo na koniec

Doświadczenie podpowiada mi, że najwięcej trudności mamy przy regułach High Cohesion i Low Coupling – to właśnie je łamiemy najczęściej. O ile do pozostałych zasad stosujemy się dość często i z reguły automatycznie, tak o tych dwóch regułach najczęściej przypominamy sobie wtedy, gdy nasz kod wygląda już nieciekawie. Dzieje się tak, ponieważ z reguły gdy mamy dodać nową funkcjonalność, to znajdujemy „odpowiednią” klasę i ją tam umieszczamy. Nie myślimy wtedy o sprzężeniu i spójności. Często jest tak, że ta nowa funkcjonalność wymaga dodania kolejnej zależności do wybranej przez nas klasy. Skutkuje to tym, że wzrasta nam sprzężenie (klasa zaczyna zależeć od kolejnego elementu), a także maleje spójność – nową zależność wykorzystujemy tylko dla tej nowej funkcjonalności. Dlatego moim zdaniem powinniśmy szczególnie pamiętać o tych dwóch zasadach. Stosowanie ich może mieć największy wpływ na nasze aplikacje i powinno uchronić nas od tego, abyśmy pewnego dnia nie obudzili się z przysłowiową ręką w nocniku.

Tagi:

1 myśl na “GRASP”

  1. Pingback: dotnetomaniak.pl

Leave a Reply