L'applicazione di composizione sull'ereditarietà ai controller è uno di quegli approcci con i quali non puoi sbagliare.
L'idea è di avere il controller più sottile possibile. Il controller dovrebbe solo definire i processi di richiesta / risposta, ma qualsiasi cosa che si verifichi nel mezzo sarà definita al di fuori della classe.
Ad esempio, se disponi di un controller che ha filtrato una raccolta di Product
istanze in base a un determinato criterio, applica l'IVA al prezzo base e quindi produci una rappresentazione tabellare, CSV o JSON, ti ritroverai con le seguenti classi:
- Una classe che accetta l'oggetto richiesta e restituisce una raccolta appropriata di istanze
Product
; questa classe si occupa di sapere come costruire una query appropriata in base alla richiesta in entrata (come l'interrogazione di prodotti basata su un mix di valori di attributo).
- Una classe che accetta una raccolta
Product
, elabora ciascuna voce e restituisce la raccolta risultante; questa classe si occupa di prendere l'IVA specificata di un prodotto e applicarla sul prezzo base.
- Una classe che accetta l'oggetto richiesta in entrata e una raccolta
Product
e produce una risposta appropriata; questa classe si occupa di capire se produrre una risposta HTML, CSV o JSON della raccolta.
- Il tuo controller attuale che crea semplicemente una rete di comunicazione tra le tre classi; questa classe specifica infine la nozione di raccolta, elaborazione e visualizzazione del modello
Product
in modo appropriato.
Il fatto è che i controller spesso presentano un sacco di comportamenti complessi, ma in questo modo il comportamento viene preso in considerazione in diversi componenti che vengono definiti e testati indipendentemente. Semplicemente mescolando in classi diverse e cambiando poche righe di codice, il comportamento del controller cambia drasticamente. Non ho alcuna esperienza con Ruby, ma da quello che so questo può essere ottenuto includendo e mescolando diversi moduli e forse anche una macro o due per il condimento.
Un buon esempio di questo approccio sono i controller generici di Django. Sono solo tipi composti da diversi mixin in cui ogni mixin definisce determinati comportamenti ed espone gli attributi di classe ei metodi di istanza che possono essere specificati / sovrascritti per configurare il loro comportamento. I controller generici forniti sono solo una particolare combinazione di mixin esistenti che sono adatti per un particolare problema.
Un altro buon esempio è Android con i suoi controller. Un Acitivty
viene chiamato dalla struttura della strumentazione ogni volta che l'attività deve rispondere a un certo tipo di richiesta (creare te stesso, iniziare te stesso, distruggere te stesso) con un determinato input, ma sta accadendo tutto su un thread dell'interfaccia utente che si aspetta che questi metodi siano elegante. Qualunque cosa sostanziale, come il numero di crunch, il recupero di dati su HTTP, la raccolta di dati dal DB, così via, viene effettivamente gestita da oggetti di alcuni tipi esterni che comunicano alcuni dati al controller chiamante. A volte, questa comunicazione è abbastanza generica da poter essere strutturata implementando interfacce e il comportamento relativo a ciascuna di esse è specificato in una classe anonima. Di nuovo, si finisce con un semplice controllore che collega solo una rete di comunicazione tra diversi oggetti per definire un comportamento complesso, ma il comportamento è definito in una forma compatta dalla quale è possibile capire facilmente cosa sta succedendo.
Questo è il riutilizzo del codice promesso dal principio di composizione: se il comportamento del controller deve essere migrato su un controller diverso o se un determinato tipo di comportamento deve essere condiviso tra più controllori, la composizione sull'ereditarietà ti dà la possibilità di fare così. E cosa c'è di meglio, applicando il principio di sostituzione di Liskov , le possibilità diventano infinite!