GRASP (ang. General Responsibility Assignment Software Patterns)
to zbiór wzorców projektowych, które mają pomóc programistom w tworzeniu dobrze zaprojektowanych, elastycznych i łatwych do utrzymania aplikacji. Wzorce te zostały opracowane przez Craig Larman, autora książki „Applying UML and Patterns”, i obejmują kilka zasad projektowania obiektowego – OOP (Object Oriented Programming).
GRASP skupia się na przypisywaniu odpowiedzialności w programowaniu obiektowym i opiera się na zasadzie pojedynczej odpowiedzialności (SRP), która mówi, że każda klasa powinna mieć tylko jeden powód do zmiany.
Wzorce GRASP opierają się na wykorzystaniu kilku technik, takich jak delegacja, wzorzec twórcy, wzorzec kontrolera, wzorzec tworzenia fabryki, wzorzec strategii i wiele innych. Każdy z tych wzorców pomaga w przypisywaniu odpowiedzialności do klas i obiektów, co z kolei prowadzi do tworzenia aplikacji, które są elastyczne, łatwe w utrzymaniu i rozszerzalne.
Warto podkreślić, że GRASP to nie tylko zbiór konkretnych wzorców projektowych, ale to także podejście do projektowania oprogramowania. GRASP koncentruje się na przypisywaniu odpowiedzialności, aby zapewnić, że klasa będzie odpowiedzialna tylko za swoje zadania i nie będzie próbowała zajmować się innymi kwestiami. Odpowiednie przypisanie odpowiedzialności pomaga w uniknięciu skomplikowanej i trudnej do zrozumienia logiki biznesowej, a także pozwala na łatwiejsze testowanie i utrzymanie aplikacji.
Wzorce GRASP mogą być z powodzeniem stosowane w języku programowania PHP, podobnie jak w innych językach obiektowych. Ich wykorzystanie może znacznie ułatwić tworzenie dobrze zaprojektowanej i elastycznej aplikacji.
Poniżej przedstawiam przykłady użycia GRASP w PHP:
- INFORMATION EXPERT (Zasada informacji ekspertów)
- CREATOR (Twórca)
- CONTROLLER (Kontroller)
- LOW COUPLING (Słaba zależność)
- HIGH COHESION (Wysoka spójność)
- POLYMORPHISM (Polimorfizm)
- PURE FABRICATION (Ustalanie punktu dostępu)
- INDIRECTION (Pośrednictwo)
- Protected Variations (Ochrona Zmienności)
INFORMATION EXPERT (Ekspert)
Zasada Eksperta Informacji jest kluczową zasadą Programowania Obiektowego (OOP). Ta zasada koncentruje się na odpowiedzialności, określając, która klasa powinna być odpowiedzialna za wykonanie konkretnego zadania. Zadanie powinno być przydzielone klasie, która dysponuje największą ilością informacji niezbędnych do jego wykonania. W skrócie, zasada ta wskazuje, że każde zadanie powinno być powierzone ekspertowi w danej dziedzinie, posiadającemu wymaganą wiedzę oraz umiejętności do jego realizacji.
// Klasa Customer reprezentuje klienta, // który posiada nazwę i adres e-mail, a także listę zamówień. class Customer { public function __construct( private $name, private $email, private $orders = [] ) { } public function getName() { return $this->name; } public function getEmail() { return $this->email; } public function addOrder(Order $order) { $this->orders[] = $order; } public function getOrdersCount() { return count($this->orders); } } // Klasa Order reprezentuje zamówienie, // które jest złożone przez klienta i zawiera informacje o produkcie i jego ilości class Order { public function __construct ( private Customer $customer; private $product; private $quantity ) { $customer->addOrder($this); } public function getCustomerName() { return $this->customer->getName(); } public function getCustomerEmail() { return $this->customer->getEmail(); } public function getProduct() { return $this->product; } public function getQuantity() { return $this->quantity; } } /** * Zgodnie z zasadą Information Expert, metody getCustomerName, getCustomerEmail, getProduct i getQuantity * zostały zdefiniowane w klasie Order, ponieważ ta klasa posiada wszystkie potrzebne informacje, * aby te metody mogły być wykonane. Ponadto, metoda addOrder została zdefiniowana * w klasie Customer, ponieważ ta klasa jest odpowiedzialna za przechowywanie zamówień klienta * i posiada najlepszą wiedzę na temat tego, jakie zamówienia zostały złożone. */
CREATOR (Twórca)
Zasada Creator (Twórca) określa, kto jest odpowiedzialny za tworzenie obiektów. Klasa B powinna tworzyć instancje klasy A, jeśli zawiera, agreguje, inicjuje dane, zapamiętuje lub intensywnie korzysta z klasy A. Na przykład, klasa Order
może tworzyć obiekty klasy LineItem
ponieważ korzysta z klasy LineItem
.
class LineItem { // ... } class Order { public function createLineItem($product, $quantity) { $lineItem = new LineItem($product, $quantity); // dodajemy lineItem do listy lineItems w Order $this->lineItems[] = $lineItem; } // ... }
CONTROLLER (Kontroler)
Klasa kontrolera powinna być odpowiedzialna za koordynację akcji wykonywanych przez wiele klas. Kontroler może korzystać z innych klas, ale nie powinien być zależny od nich. Na przykład, klasa ShoppingCartController
może być odpowiedzialna za koordynowanie akcji wykonywanych przez klasy Cart
i Product
.
Wykorzystanie kontrolera w koncepcji GRASP może odnosić się do jego roli w architekturze MVC, gdzie odseparowanie logiki biznesowej od warstwy prezentacji jest kluczowe dla utrzymania czytelności, łatwości zarządzania i rozszerzalności aplikacji.
class ShoppingCartController { public function addToCart($product, $quantity) { // tworzymy obiekt klasy Cart $cart = new Cart(); // dodajemy produkt do koszyka $cart->addItem($product, $quantity); // zapisujemy koszyk do bazy danych $cart->save(); } // ... }
LOW COUPLING (Słaba zależność)
Wzorzec ten mówi, że klasy powinny być ze sobą słabo powiązane. To znaczy, że zmiany w jednej klasie nie powinny wpływać na wiele innych klas.
Aby to osiągnąć, klasa powinna korzystać z innych klas poprzez interfejsy.
Stosowanie tej zasady jest kluczowe, ponieważ pomaga ograniczyć wpływ zmian w aplikacji oraz zmniejsza ryzyko wystąpienia błędów. Dzięki temu możliwe jest lepsze zrozumienie kodu i ułatwienie jego modyfikacji, co wpływa korzystnie na długoterminową jakość projektu.
interface ProductInterface { public function getPrice(); } class Product implements ProductInterface { public function getPrice() { // ... } // ... } class ShoppingCart { public function addItem(ProductInterface $product, $quantity) { // ... } // ... }
HIGH COHESION (Wysoka spójność)
Wzorzec ten mówi, że metody w klasie powinny być ze sobą mocno powiązane. Oznacza to, że metody w klasie powinny mieć wspólny cel i działać na podobnych danych.
Załóżmy, że mamy klasę User
, która ma odpowiedzialność za zarządzanie danymi użytkownika (zgodnie z SRP SOLID). Aby zapewnić wysoką spójność, klasa powinna zajmować się tylko operacjami związanymi z użytkownikiem.
class User { private $userId; private $username; private $email; public function __construct($userId, $username, $email) { $this->userId = $userId; $this->username = $username; $this->email = $email; } public function getUsername() { return $this->username; } public function getEmail() { return $this->email; } public function updateUserDetails($username, $email) { // Metoda do aktualizacji danych użytkownika // ... } public function deleteUser() { // Metoda do usuwania użytkownika // ... } }
W tym przykładzie klasa User
ma metody związane wyłącznie z operacjami na danych użytkownika, takimi jak pobieranie nazwy użytkownika, adresu e-mail czy aktualizacja/usuwanie danych. Wysoka spójność oznacza, że klasa ta skupia się tylko na jednym konkretnym aspekcie (zarządzaniu użytkownikami) i nie zajmuje się zbędnymi czynnościami.
Dzięki temu, w miarę rozwoju projektu czy potrzeb dodania nowych funkcjonalności, zmiany w klasie User
będą dotyczyły głównie operacji na danych użytkownika, co ułatwia zarządzanie kodem i zapewnia jego spójność.
POLYMORPHISM (polimorfizm)
Jest to jedna z najważniejszych cech OOP, mówiąca o tym, że klasy powinny dziedziczyć po innych klasach lub implementować interfejsy w celu uzyskania elastyczności w tworzeniu kodu.
Polimorfizm w GRASP odnosi się do zdolności różnych klas do udostępniania wspólnego interfejsu dla zachowania, ale w kontekście swojego własnego funkcjonowania. To oznacza, że różne klasy mogą interpretować tę samą metodę w różny sposób, zależnie od swoich potrzeb, ale z zachowaniem wspólnego interfejsu.
Jaka jest różnica między zasadą Poliformizmu, a Low couplingu?
Low Coupling (Słaba zależność) dotyczy minimalizowania zależności między różnymi obiektami lub klasami w systemie. Ma to na celu zmniejszenie powiązań między nimi, dzięki czemu system staje się bardziej elastyczny i łatwiejszy w modyfikacji. Słaba zależność redukuje ryzyko powstawania błędów spowodowanych zmianami w jednej klasie, które mogłyby wpłynąć na wiele innych miejsc w systemie.
Polimorfizm dotyczy różnych zachowań w ramach wspólnego interfejsu, podczas gdy low coupling koncentruje się na minimalizowaniu zależności między różnymi elementami systemu. Oba są ważne dla utrzymania elastycznego, skalowalnego i łatwego w modyfikacji kodu. Polimorfizm może być jednym z narzędzi, które pomagają w tworzeniu niskiego sprzężenia poprzez zapewnienie jednolitego interfejsu dla różnych klas lub obiektów.
Poniżej przykład poliformizmu:
interface ProductInterface { public function getPrice(); } class Product implements ProductInterface { public function getPrice() { // ... } // ... } class DiscountedProduct extends Product { private $discount; public function setDiscount($discount) { $this->discount = $discount; } // nadpisanie metody getPrice() z klasy bazowej public function getPrice() { $price = parent::getPrice(); $price = $price - ($price * ($this->discount / 100)); return $price; } // ... }
Pure Fabrication (Ustalanie punktu dostępu)
Zasada „Ustalanie punktu dostępu (Pure Fabrication)” odnosi się do tworzenia sztucznych klas lub obiektów, które nie reprezentują żadnego konkretnego elementu systemu, lecz są wykorzystywane do utrzymania spójności i niskiego stopnia powiązań między komponentami systemu.
Załóżmy, że mamy system e-commerce, gdzie istnieje potrzeba zarządzania koszykiem zakupowym użytkownika. Zgodnie z zasadą „Ustalanie punktu dostępu”, możemy stworzyć sztuczną klasę CartManager
, która będzie odpowiadać za zarządzanie koszykiem zakupów. Ta klasa niekoniecznie reprezentuje fizyczny element systemu (jak produkt czy użytkownik), ale jest używana do koordynacji i zarządzania koszykiem.
class CartManager { private $cartItems = []; public function addItem($item) { // Logika dodawania przedmiotu do koszyka $this->cartItems[] = $item; echo "Przedmiot dodany do koszyka."; } // Inne metody do zarządzania koszykiem }
Indirection (Pośrednictwo)
Wzorzec Dostawcy/ Pośrednictwa (Indirection) mówi o wykorzystaniu pośrednictwa lub fasad do zarządzania komunikacją między różnymi klasami w systemie. Wzorzec ten pomaga w minimalizowaniu bezpośrednich powiązań między komponentami.
interface MessageSender { public function sendMessage($message); } class EmailService implements MessageSender { public function sendMessage($message) { // Logika wysyłania emaila echo "Wysłano email: $message"; } } class SMSService implements MessageSender { public function sendMessage($message) { // Logika wysyłania SMSa echo "Wysłano SMS: $message"; } } class NotificationService { private $messageSender; public function __construct(MessageSender $messageSender) { $this->messageSender = $messageSender; } public function sendNotification($message) { $this->messageSender->sendMessage($message); } } // Użycie $emailSender = new EmailService(); $smsSender = new SMSService(); $emailNotification = new NotificationService($emailSender); $smsNotification = new NotificationService($smsSender); $emailNotification->sendNotification("Nowa wiadomość email"); $smsNotification->sendNotification("Nowa wiadomość SMS");
W tym przykładzie NotificationService
działa jako punkt dostępu do wysyłania wiadomości poprzez interfejs MessageSender
, który może być implementowany przez różne usługi wysyłania wiadomości (EmailService
, SMSService
). To pośrednictwo pozwala na elastyczne korzystanie z różnych usług wysyłania wiadomości bez konieczności bezpośredniego kontaktu z nimi przez inne komponenty systemu.
Protected Variations (Ochrona Zmienności)
Reguła protected variations chroni elementy przed zmianami w innych elementach (obiektach, systemach, podsystemach) poprzez opakowanie obszaru niepewności interfejsem i wykorzystanie polimorfizmu do stworzenia różnych implementacji tego interfejsu.
Problem: Jak zaprojektować obiekty, podsystemy i systemy, aby zmiany lub niepewności w tych elementach nie miały niepożądanego wpływu na inne elementy? Rozwiązanie: Zidentyfikować obszary przewidywanych zmian lub niepewności; przydzielić odpowiedzialności w celu stworzenia stabilnego interfejsu wokół tych obszarów.
Załóżmy, że mamy system obsługujący różne sposoby płatności. Zgodnie z zasadą „Chronienia przed zmianami”, możemy stworzyć interfejs PaymentMethod
jako punkt stabilności, który będzie opakowywał różne sposoby płatności.
interface PaymentMethod { public function processPayment($amount); } class CardPayment implements PaymentMethod { public function processPayment($amount) { // Logika płatności kartą echo "Płatność kartą za $amount zł"; } } class PayPalPayment implements PaymentMethod { public function processPayment($amount) { // Logika płatności przez PayPal echo "Płatność przez PayPal za $amount zł"; } } // Klasa obsługująca płatności class PaymentProcessor { private $paymentMethod; public function __construct(PaymentMethod $paymentMethod) { $this->paymentMethod = $paymentMethod; } public function process($amount) { $this->paymentMethod->processPayment($amount); } } // Użycie $cardPayment = new CardPayment(); $paypalPayment = new PayPalPayment(); $paymentProcessor1 = new PaymentProcessor($cardPayment); $paymentProcessor1->process(100); $paymentProcessor2 = new PaymentProcessor($paypalPayment); $paymentProcessor2->process(50);
W tym przypadku PaymentMethod
stanowi punkt stabilności, a klasy CardPayment
oraz PayPalPayment
są różnymi implementacjami tego interfejsu. Dzięki temu, gdy zmienia się sposób płatności (np. dodanie nowej metody płatności), nie ma to wpływu na inne części systemu, ponieważ wszystkie implementacje korzystają z tego samego interfejsu PaymentMethod
.
Nikt jeszcze nie komentował. Bądź pierwszy!