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.
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 kilka przykładów wzorców GRASP w języku PHP:
CREATOR (kreator)
Wzorzec ten mówi, że klasa powinna tworzyć obiekty innych klas. Na przykład, klasa Order może tworzyć obiekty 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.
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.
interface IProduct { public function getPrice(); } class Product implements IProduct { public function getPrice() { // ... } // ... } class ShoppingCart { public function addItem(IProduct $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.
class Product { private $id; private $name; private $price; public function setId($id) { $this->id = $id; } public function setName($name) { $this->name = $name; } public function setPrice($price) { $this->price = $price; } // metoda zwracająca cenę produktu wraz z podatkiem VAT public function getPriceWithVAT() { return $this->price * 1.23; } // ... }
POLYMORPHISM (polimorfizm)
Wzorzec ten mówi, że klasy powinny dziedziczyć po innych klasach lub implementować interfejsy w celu uzyskania elastyczności w tworzeniu kodu.
interface IProduct { public function getPrice(); } class Product implements IProduct { 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; } // ... }
INFORMATION EXPERT (zasada informacji ekspertów)
Zadanie powinno być powierzone klasie, która ma najwięcej informacji potrzebnych do jego wykonania. Innymi słowy, ta zasada wskazuje, że każde zadanie powinno być powierzone ekspertowi od danego zagadnienia, który posiada odpowiednią wiedzę i umiejętności do jego wykonania.
// 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. */
DELEGATION (wskaźnik delegacji)
Klasa powinna delegować swoje zadania do innych klas, które mają więcej informacji potrzebnych do ich wykonania. Przykładowo, można zastosować tę zasadę, gdy dana klasa potrzebuje dostępu do funkcjonalności innej klasy, ale nie powinna ich implementować samodzielnie.
class Database { public function save($data) { // kod do zapisania danych do bazy danych } } // Klasa User deleguje zadanie zapisu danych do bazy danych do klasy Database. // Nie musi wiedzieć jak dokładnie działa mechanizm zapisu. class User { private $db; public function __construct(Database $db) { $this->db = $db; } public function saveData($data) { // przekazanie zadania do klasy Database za pomocą delegacji $this->db->save($data); } } // Przykład użycia: $db = new Database(); $user = new User($db); $user->saveData($data);
HIGH-LEVEL DESIGN (metoda wstępująca)
Klasa powinna definiować algorytm, ale poszczególne kroki algorytmu powinny być realizowane przez inne klasy. To znaczy, że klasy powinny być zaprojektowane od góry do dołu, zaczynając od wysokiego poziomu abstrakcji i stopniowo schodząc na niższy poziom.
// Interfejs Logger, który definiuje metodę log, która będzie implementowana przez wszystkie // klasy, które służą do logowania informacji. interface Logger { public function log($message); } // klasy, FileLogger i EmailLogger, które implementują interfejs Logger. // Klasa FileLogger loguje informacje do pliku, a klasa EmailLogger wysyła informacje // logowania na podany adres e-mail. class FileLogger implements Logger { public function __construct(private $filename) { } public function log($message) { file_put_contents($this->filename, $message . PHP_EOL, FILE_APPEND); } } class EmailLogger implements Logger { public function __construct ( private $recipient ) { } public function log($message) { mail($this->recipient, 'Log Message', $message); } } // Klasa User, która reprezentuje użytkownika. Klasa ta przyjmuje obiekt klasy implementującej // interfejs Logger w konstruktorze. Metody login i logout logują informacje o logowaniu // i wylogowaniu użytkownika za pomocą obiektu loggera przekazanego do konstruktora. class User { public function __construct( private $name, private Logger $logger ) { } public function login() { $this->logger->log("User '{$this->name}' logged in."); } public function logout() { $this->logger->log("User '{$this->name}' logged out."); } } /** * W tym przykładzie klasy są zaprojektowane od góry do dołu, zaczynając od interfejsu Logger, * który jest najwyższym poziomem abstrakcji, a następnie schodząc na niższy poziom * z klasami FileLogger, EmailLogger i User, które implementują interfejs Logger. */
ABSTRACT FACTORY (fabryka abstrakcyjna)
Klasa powinna służyć do tworzenia obiektów powiązanych ze sobą, takich jak obiekty związane z jednym modelem biznesowym. Wszystkie obiekty utworzone przez fabrykę powinny implementować ten sam interfejs.
// W tym przykładzie mamy interfejs Button, który reprezentuje przycisk. // Mamy dwie klasy, MacButton i WindowsButton, które implementują interfejs Button. interface Button { public function paint(); } class MacButton implements Button { public function paint() { return "Rendering a Mac Button\n"; } } class WindowsButton implements Button { public function paint() { return "Rendering a Windows Button\n"; } } /** * Interfejs GUIFactory, który definiuje metodę createButton, która będzie zwracała obiekt * klasy implementującej interfejs Button. Mamy dwie klasy, MacFactory i WindowsFactory, * które implementują interfejs GUIFactory. Każda fabryka tworzy obiekty przycisków * odpowiednie dla określonego systemu operacyjnego. */ interface GUIFactory { public function createButton(): Button; } class MacFactory implements GUIFactory { public function createButton(): Button { return new MacButton(); } } class WindowsFactory implements GUIFactory { public function createButton(): Button { return new WindowsButton(); } } // Klasa Application, która przyjmuje obiekt klasy implementującej interfejs GUIFactory // w konstruktorze. Metoda createUI tworzy obiekt przycisku za pomocą fabryki // i wywołuje jego metodę paint, która wyświetla komunikat na ekranie. class Application { public function __construct(private GUIFactory $factory) { } public function createUI() { $button = $this->factory->createButton(); $button->paint(); } } $os = "mac"; // or "windows" $factory = match ($os) { 'mac' => new MacFactory(), 'windows' => new WindowsFactory(), default => 'Unknown operating system' }; if (!$factory instanceof GUIFactory) { die("Unknown operating system"); } // W tym przykładzie fabryka tworzy obiekty bez specyfikowania ich klas, // a wszystkie obiekty implementują ten sam interfejs Button. $app = new Application($factory); $app->createUI();
FACTORY METHOD (metoda fabrykująca)
Klasa powinna być odpowiedzialna za tworzenie obiektów, ale nie powinna określać, które obiekty są tworzone.
Lepiej zrozumieć Abstract Factory i Factory Method
Zasady GRASP Factory Method i Abstract Factory dotyczą obu sposobów tworzenia obiektów, ale różnią się w sposób, w jaki są używane i implementowane.
Zasada Factory Method mówi, że fabryka powinna być abstrakcyjną klasą bazową, która definiuje interfejs do tworzenia obiektów, ale pozostawia implementację tworzenia obiektów klasom pochodnym. W ten sposób klasa bazowa określa sposób tworzenia obiektów, ale pozostawia szczegóły implementacji klasom pochodnym, które same decydują, jakie obiekty utworzyć. Fabryka tworzy jeden typ produktu.
Z drugiej strony, zasada Abstract Factory mówi, że fabryka abstrakcyjna definiuje interfejs do tworzenia rodzin powiązanych ze sobą obiektów bez określania ich konkretnych klas. W ten sposób fabryka abstrakcyjna umożliwia tworzenie różnych produktów, które ze sobą współpracują. W odróżnieniu od Factory Method, gdzie fabryka tworzy jeden typ produktu, Abstract Factory tworzy całe rodziny produktów.
Podsumowując, Factory Method umożliwia tworzenie jednego typu produktu, ale pozwala na elastyczność w jego tworzeniu przez klasy pochodne. Abstract Factory pozwala na tworzenie różnych typów produktów, ale wymaga utworzenia nowej fabryki abstrakcyjnej, jeśli chcesz stworzyć nową rodzinę powiązanych ze sobą produktów.
Nikt jeszcze nie komentował. Bądź pierwszy!