Vai al contenuto

I principi GRASP con esempi in PHP

Hai mai sentito parlare di GRASP?

GRASP è un concetto tipicamente accademico, che si approfondisce di solito nei corsi di ingegneria del software. E’ l’acronimo di General Responsibility Assignment Software Patterns, cioè un insieme di principi e linee guida che aiutano nella progettazione e assegnazione di responsabilità all’interno di un sistema software. Questi principi sono stati introdotti da Craig Larman nel suo libro “Applying UML and Patterns” come modelli per migliorare la progettazione orientata agli oggetti.

L’adozione di questi principi può fornire una guida utile nella progettazione di software OOP, contribuendo a creare sistemi più modulari, comprensibili e facilmente mantenibili.

Di seguito illustrerà una panoramica dei principi GRASP con le relative applicazioni in linguaggio PHP. Da notare che il linguaggio è stato scelto per pura comodità e prescinde dai concetti applicati, per cui si potrebbe utilizzare qualsiasi tipo di linguaggio.

Information Expert

Questo principio suggerisce che la responsabilità dovrebbe essere assegnata alla classe che possiede le informazioni necessarie. Questo migliora l’incapsulamento, consentendo alle classi di mantenere la loro integrità e proteggere le informazioni sensibili. Inoltre, promuove la coesione, in quanto le informazioni pertinenti sono gestite da una singola classe.

class User {
    private $username;

    public function __construct($username) {
        $this->username = $username;
    }

    // Information Expert: la classe User possiede le informazioni sul nome utente
    public function getUsername() {
        return $this->username;
    }
}

// Utilizzo
$user = new User('pippo');
$username = $user->getUsername();

Creator

Il principio del Creator aiuta a evitare l’accoppiamento eccessivo tra classi, suggerendo che la responsabilità di creare un’istanza di una classe dovrebbe appartenere a una classe correlata. Questo favorisce la modularità e facilita la manutenzione del codice quando è necessario apportare modifiche alle istanze create.

class UserManager {
    // Creator: la classe UserManager è responsabile della creazione di istanze di User
    public function createUser($username) {
        return new User($username);
    }
}

// Utilizzo
$userManager = new UserManager();
$newUser = $userManager->createUser('pippo');

Controller

Assegnando la responsabilità di gestire gli eventi del sistema a una classe controller, si promuove l’organizzazione e il coordinamento delle interazioni tra le classi. Questo principio è particolarmente utile nelle applicazioni basate su eventi o nelle architetture MVC (Model-View-Controller) ed è alla base del meccanismo di funzionamento di alcuni framework di sviluppo, tra cui Symfony.

class UserController {
    // Controller: la classe UserController è responsabile della gestione degli eventi utente
    public function handleUserEvent($event) {
        // Logica di gestione degli eventi utente
    }
}

// Utilizzo
$userController = new UserController();
$userController->handleUserEvent($someEvent);

Low Coupling

La riduzione delle dipendenze tra le classi è fondamentale per ottenere un sistema flessibile e manutenibile. L’obiettivo è minimizzare l’impatto delle modifiche in una classe sulle altre. L’utilizzo di interfacce, design pattern come Dependency Injection e l’adesione ai principi SOLID sono comuni strategie per mantenere un basso accoppiamento.

// Low Coupling: Dipendenza tramite interfaccia
interface LoggerInterface {
    public function log($message);
}

class FileLogger implements LoggerInterface {
    public function log($message) {
        // Logica che lavora sul file di log
    }
}

class UserManager {
    private $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }

    public function createUser($username) {
        // Creazione di un nuovo utente
        $this->logger->log("Nuovo utente creato: $username");
        return new User($username);
    }
}

// Utilizzo
$fileLogger = new FileLogger();
$userManager = new UserManager($fileLogger);
$newUser = $userManager->createUser('pippo');

High Cohesion

Questo principio incoraggia a raggruppare le responsabilità correlate all’interno di una classe, evitando che una classe abbia troppe responsabilità diverse. Classi con alta coesione sono più comprensibili, facilmente manutenibili e meno soggette a errori.

class ShoppingCart {
    private $items = [];

    // Alta coesione: tutte le operazioni correlate al carrello sono in una sola classe
    public function addItem($item) {
        $this->items[] = $item;
    }

    public function calculateTotal() {
        // Logica per calcolare il totale degli articoli nel carrello
    }

    public function checkout() {
        // Logica per effettuare il checkout
    }
}

// Utilizzo
$cart = new ShoppingCart();
$cart->addItem($item1);
$cart->addItem($item2);
$total = $cart->calculateTotal();
$cart->checkout();

Polymorphism

L’utilizzo del polimorfismo consente di scrivere codice più flessibile e adattabile. Trattare oggetti diversi attraverso un’interfaccia comune semplifica l’aggiunta di nuovi tipi di oggetti senza dover modificare il codice esistente.

Per illustrare il principio GRASP del Polymorphism in PHP, vediamo un esempio che coinvolge l’uso di una classe di interfaccia e diverse classi concrete che la implementano. L’obiettivo è trattare oggetti diversi attraverso un’interfaccia comune.

// Interfaccia comune
interface Shape {
    public function calculateArea();
}

// Classi concrete che implementano l'interfaccia Shape
class Circle implements Shape {
    private $radius;

    public function __construct($radius) {
        $this->radius = $radius;
    }

    public function calculateArea() {
        return pi() * pow($this->radius, 2);
    }
}

class Rectangle implements Shape {
    private $width;
    private $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }

    public function calculateArea() {
        return $this->width * $this->height;
    }
}

class Triangle implements Shape {
    private $base;
    private $height;

    public function __construct($base, $height) {
        $this->base = $base;
        $this->height = $height;
    }

    public function calculateArea() {
        return 0.5 * $this->base * $this->height;
    }
}

// Funzione che usa Polymorphism per calcolare l'area di qualsiasi forma
function printArea(Shape $shape) {
    echo 'Area: ' . $shape->calculateArea() . PHP_EOL;
}

// Utilizzo delle classi con Polymorphism
$circle = new Circle(5);
$rectangle = new Rectangle(4, 6);
$triangle = new Triangle(3, 8);

// Chiamate alla funzione printArea con oggetti di diverse classi
printArea($circle);     // Stampa l'area del cerchio
printArea($rectangle);  // Stampa l'area del rettangolo
printArea($triangle);   // Stampa l'area del triangolo

Pure Fabrication

L’introduzione di classi fittizie (che non rappresentano concetti del dominio) può essere utile per migliorare la struttura del sistema, mantenendo al contempo un basso accoppiamento. Tuttavia, è importante bilanciare questa pratica per evitare l’introduzione di complessità inutile.

// Pure Fabrication: classe fittizia per la gestione dell'invio di email
class EmailService {
    public function sendEmail($to, $subject, $message) {
        // Logica per inviare un'email
        echo "Email inviata a $to con soggetto '$subject': $message" . PHP_EOL;
    }
}

class Order {
    private $orderId;
    private $totalAmount;
    private $customerEmail;

    public function __construct($orderId, $totalAmount, $customerEmail) {
        $this->orderId = $orderId;
        $this->totalAmount = $totalAmount;
        $this->customerEmail = $customerEmail;
    }

    public function processOrder() {
        // Logica per elaborare l'ordine

        // Pure Fabrication: utilizzo della classe EmailService per inviare una notifica
        $emailService = new EmailService();
        $emailService->sendEmail($this->customerEmail, 'Conferma ordine', 'Grazie per il tuo ordine!');
    }
}

// Utilizzo delle classi
$order = new Order(123, 100.00, 'cliente@test.com');
$order->processOrder();

In questo esempio, la classe EmailService rappresenta una classe fittizia introdotta per gestire l’invio di email. Questa classe non rappresenta direttamente un concetto del dominio specifico, ma è creata per fornire una funzionalità utile al sistema (invio di email).

La classe Order invece rappresenta un’entità del dominio, ma durante il processo dell’ordine, fa uso della classe EmailService come un’istanza di “Pure Fabrication” per inviare una notifica email al cliente. Questo aiuta a mantenere la separazione delle responsabilità e la modularità del sistema.

Indirection

Questo principio suggerisce di utilizzare classi indirette per mediatizzare le interazioni tra altri componenti o servizi. Ciò riduce l’accoppiamento diretto tra le classi coinvolte e facilita eventuali modifiche future nel flusso di controllo.

// Classe di servizio indiretta per la gestione delle notifiche
class NotificationService {
    public function sendNotification($user, $message) {
        // Logica per inviare notifiche
        echo "Notifica inviata a {$user}: {$message}" . PHP_EOL;
    }
}

// Classe che gestisce il processo di ordine
class OrderProcessor {
    private $ns;

    public function __construct(NotificationService $notificationService) {
        $this->ns = $notificationService;
    }

    public function processOrder($user, $orderId) {
        // Logica per elaborare l'ordine

        // Utilizzo dell'indirezione: chiamiamo la classe indiretta per inviare una notifica
        $this->ns->sendNotification($user, "Il tuo ordine (ID: {$orderId}) è stato processato.");
    }
}

// Utilizzo delle classi con l'Indirection
$notificationService = new NotificationService();
$orderProcessor = new OrderProcessor($notificationService);
$orderProcessor->processOrder('customer@example.com', 123);

In questo esempio, la classe NotificationService rappresenta una classe indiretta. La classe OrderProcessor è responsabile dell’ordine e utilizza NotificationService per inviare le notifiche. Questo segue il principio di Indirection, in quanto OrderProcessor non interagisce direttamente con la logica di invio delle notifiche, ma utilizza la classe indiretta NotificationService.

L’utilizzo dell’Indirection aiuta a mantenere un basso accoppiamento tra le classi coinvolte. Se in futuro dovessero cambiare i dettagli dell’implementazione della gestione delle notifiche, sarà sufficiente modificare la classe NotificationService senza dover apportare modifiche dirette alla classe OrderProcessor.

Questi sono esempi semplificati per illustrare l’applicazione di alcuni principi GRASP in PHP. Si noti che in contesti più complessi, l’applicazione di questi principi può richiedere un design più accurato e una considerazione attenta delle relazioni tra le classi.

Protected Variations

Il principio di “Protected Variations” è focalizzato sulla protezione degli elementi chiave del sistema da variazioni nelle implementazioni degli altri elementi, fornendo un’interfaccia stabile che agisce come uno strato protettivo tra le variazioni nella struttura interna del componente e i componenti esterni che ne dipendono. Gli adapter e i bridge possono essere utilizzati per adattare o intermediare tra componenti specifici e quelli che dipendono da essi, fornendo un livello di indirezione.

// Interfaccia stabile che rappresenta un servizio di pagamento
interface PaymentService {
    public function processPayment($amount);
}

// Implementazione concreta del servizio di pagamento
class CreditCardPaymentService implements PaymentService {
    public function processPayment($amount) {
        // Logica specifica per elaborare i pagamenti con carta di credito
        echo "Pagamento processato con carta di credito: $amount" . PHP_EOL;
    }
}

// Classe che utilizza il servizio di pagamento
class ShoppingCart {
    private $paymentService;

    public function __construct(PaymentService $paymentService) {
        $this->paymentService = $paymentService;
    }

    public function checkout($totalAmount) {
        // Logica del checkout

        // Utilizzo del servizio di pagamento senza preoccuparsi dei dettagli di implementazione
        $this->paymentService->processPayment($totalAmount);
    }
}

// Utilizzo delle classi
$creditCardPaymentService = new CreditCardPaymentService();
$shoppingCart = new ShoppingCart($creditCardPaymentService);
$shoppingCart->checkout(100.00);

In questo esempio, l’interfaccia PaymentService rappresenta uno strato stabile che “protegge” la classe ShoppingCart dai dettagli specifici di implementazione del servizio di pagamento. La classe ShoppingCart dipende solo dall’interfaccia stabile, consentendo una facile sostituzione con nuove implementazioni di servizi di pagamento senza dover modificare il codice della classe ShoppingCart.

Conclusione

L’applicazione di questi principi contribuisce a creare un design orientato agli oggetti più robusto, flessibile e manutenibile. Tuttavia, è importante considerare che i principi GRASP non forniscono soluzioni specifiche, ma piuttosto linee guida che possono essere adattate alle specifiche esigenze del progetto e del dominio. L’esperienza e la comprensione del contesto sono essenziali per applicare con successo questi principi nella progettazione del software.