La Single Responsibility (SRP), Open / Closed (OCP), Sostituzione di Liskov, Segregazione dell'interfaccia, e dipendenza da inversione. Cinque principi agili che dovrebbero guidarti ogni volta che scrivi il codice.
Poiché sia il principio di sostituzione di Liskov (LSP) che il principio di segregazione dell'interfaccia (ISP) sono abbastanza facili da definire ed esemplificare, in questa lezione parleremo di entrambi.
Le classi figlie non dovrebbero mai rompere le definizioni di tipo della classe genitore.
Il concetto di questo principio è stato introdotto da Barbara Liskov in una conferenza del 1987 e successivamente pubblicato in un documento insieme a Jannette Wing nel 1994. La loro definizione originale è la seguente:
Sia q (x) una proprietà dimostrabile sugli oggetti x di tipo T. Quindi q (y) dovrebbe essere dimostrabile per gli oggetti y di tipo S dove S è un sottotipo di T.
In seguito, con la pubblicazione dei principi SOLID di Robert C. Martin nel suo libro Agile Software Development, Principles, Patterns and Practices e poi ripubblicato nella versione C # del libro Agile Principles, Patterns and Practices in C #, la definizione divenne noto come Principio di sostituzione di Liskov.
Questo ci porta alla definizione data da Robert C. Martin:
I sottotipi devono essere sostituibili ai loro tipi di base.
Per quanto semplice, una sottoclasse dovrebbe sovrascrivere i metodi della classe genitore in un modo che non interrompa la funzionalità dal punto di vista del cliente. Ecco un semplice esempio per dimostrare il concetto.
class Vehicle function startEngine () // Funzionalità di avvio del motore predefinito function accelerate () // Funzionalità di accelerazione predefinita
Dato una lezione Veicolo
- può essere astratto e due implementazioni:
la classe Car estende Vehicle function startEngine () $ this-> engageIgnition (); parent :: startEngine (); private function engagementIgnition () // Procedura di accensione classe ElectricBus estende Vehicle function accelerate () $ this-> increaseVoltage (); $ This-> connectIndividualEngines (); funzione privata increaseVoltage () // logica elettrica funzione privata connectIndividualEngines () // logica di connessione
Una classe client dovrebbe essere in grado di utilizzare uno di essi, se può utilizzare Veicolo
.
class Driver function go (Veicolo $ v) $ v-> startEngine (); $ V-> accelerare ();
Il che ci porta ad una semplice implementazione del Pattern Method Design Pattern come lo abbiamo usato nel tutorial OCP.
Sulla base della nostra precedente esperienza con il Principio Aperto / Chiuso, possiamo concludere che il Principio di sostituzione di Liskov è in forte relazione con l'OCP. Infatti, "una violazione di LSP è una violazione latente di OCP" (Robert C. Martin), e il Template Method Design Pattern è un classico esempio di rispetto e implementazione di LSP, che a sua volta è una delle soluzioni per rispettare OCP anche.
Per illustrare completamente questo, andremo con un esempio classico perché è altamente significativo e facilmente comprensibile.
class Rectangle private $ topLeft; $ larghezza privata; altezza $ privata; funzione pubblica setHeight ($ height) $ this-> height = $ height; public function getHeight () return $ this-> height; public function setWidth ($ width) $ this-> width = $ width; public function getWidth () return $ this-> width;
Iniziamo con una forma geometrica di base, a Rettangolo
. È solo un semplice oggetto dati con setter e getter per larghezza
e altezza
. Immagina che la nostra applicazione funzioni e sia già distribuita a diversi client. Ora hanno bisogno di una nuova funzionalità. Devono essere in grado di manipolare i quadrati.
Nella vita reale, in geometria, un quadrato è una forma particolare di rettangolo. Quindi potremmo provare a implementare a Piazza
classe che estende a Rettangolo
classe. Si dice spesso che una classe di bambini è un classe genitore, e questa espressione è conforme anche a LSP, almeno a prima vista.
Ma è un Piazza
davvero un Rettangolo
in programmazione?
class square estende Rectangle public function setHeight ($ value) $ this-> width = $ value; $ this-> height = $ value; public function setWidth ($ value) $ this-> width = $ valore; $ this-> height = $ value;
Un quadrato è un rettangolo con larghezza e altezza uguali e potremmo fare una strana implementazione come nell'esempio precedente. Potremmo sovrascrivere entrambi i setter per impostare l'altezza e la larghezza. Ma come questo influirebbe sul codice cliente?
class Client function areaVerifier (Rectangle $ r) $ r-> setWidth (5); $ R-> setHeight (4); if ($ r-> area ()! = 20) lanciare una nuova eccezione ('Bad area!'); return true;
È concepibile avere una classe client che verifica l'area del rettangolo e genera un'eccezione se è sbagliata.
function area () return $ this-> width * $ this-> height;
Naturalmente abbiamo aggiunto il metodo sopra al nostro Rettangolo
classe per fornire l'area.
class LspTest estende PHPUnit_Framework_TestCase function testRectangleArea () $ r = new Rectangle (); $ c = nuovo Client (); $ This-> assertTrue ($ c-> areaVerifier ($ r));
E abbiamo creato un semplice test inviando un oggetto rettangolo vuoto al verificatore di area e ai passaggi di test. Se il nostro Piazza
la classe è definita correttamente, inviandola al Cliente areaVerifier ()
non dovrebbe rompere la sua funzionalità. Dopo tutto, a Piazza
è un Rettangolo
in tutti i sensi matematici. Ma è la nostra classe?
function testSquareArea () $ r = new Square (); $ c = nuovo Client (); $ This-> assertTrue ($ c-> areaVerifier ($ r));
Provarlo è molto semplice e si rompe alla grande. Un'eccezione ci viene lanciata quando eseguiamo il test di cui sopra.
PHPUnit 3.7.28 di Sebastian Bergmann. Eccezione: area danneggiata! # 0 / paht /: / ... / ... /LspTest.php(18): Client-> areaVerifier (Object (Square)) # 1 [funzione interna]: LspTest-> testSquareArea ()
Quindi, il nostro Piazza
la classe non è un Rettangolo
Dopotutto. Rompe le leggi della geometria. Fallisce e viola il Principio di sostituzione di Liskov.
In particolare, amo questo esempio perché non solo viola LSP, ma dimostra anche che la programmazione orientata agli oggetti non riguarda la mappatura della vita reale agli oggetti. Ogni oggetto nel nostro programma deve essere un'astrazione su un concetto. Se proviamo a mappare oggetti reali uno-a-uno a oggetti programmati, quasi sempre falliremo.
Il principio di responsabilità unica riguarda gli attori e l'architettura di alto livello. Il Principio Aperto / Chiuso riguarda la progettazione della classe e le estensioni delle funzionalità. Il principio di sostituzione di Liskov riguarda la sottotipizzazione e l'ereditarietà. L'Interfaccia Segregation Principle (ISP) riguarda la business logic per la comunicazione dei clienti.
In tutte le applicazioni modulari deve esserci una sorta di interfaccia su cui il cliente può fare affidamento. Queste possono essere entità tipizzate con interfaccia reale o altri oggetti classici che implementano modelli di progettazione come Facciate. Non importa quale sia la soluzione utilizzata. Ha sempre lo stesso scopo: comunicare al codice cliente su come usare il modulo. Queste interfacce possono risiedere tra diversi moduli nella stessa applicazione o progetto, o tra un progetto come una libreria di terze parti che serve un altro progetto. Di nuovo, non importa. La comunicazione è comunicazione e i clienti sono clienti, indipendentemente dalle persone fisiche che scrivono il codice.
Quindi, come dovremmo definire queste interfacce? Potremmo pensare al nostro modulo ed esporre tutte le funzionalità che vogliamo che offra.
Questo sembra un buon inizio, un ottimo modo per definire ciò che vogliamo implementare nel nostro modulo. O è? Un inizio come questo porterà a una delle due possibili implementazioni:
Auto
o Autobus
classe che implementa tutti i metodi su Veicolo
interfaccia. Solo le dimensioni di tali classi dovrebbero dirci di evitarli a tutti i costi.LightsControl
, Controllo di velocità
, o RadioCD
che stanno implementando l'intera interfaccia, ma in realtà forniscono qualcosa di utile solo per le parti che implementano.È ovvio che nessuna delle due soluzioni è accettabile per implementare la nostra logica aziendale.
Potremmo prendere un altro approccio. Rompi l'interfaccia in pezzi, specializzati in ogni implementazione. Ciò aiuterebbe a utilizzare classi piccole che si preoccupano della propria interfaccia. Gli oggetti che implementano le interfacce saranno utilizzati dai diversi tipi di veicoli, come la macchina nell'immagine sopra. L'auto utilizzerà le implementazioni ma dipenderà dalle interfacce. Quindi uno schema come quello qui sotto potrebbe essere ancora più espressivo.
Ma questo cambia radicalmente la nostra percezione dell'architettura. Il Auto
diventa il cliente invece dell'implementazione. Vogliamo ancora offrire ai nostri clienti modi per utilizzare l'intero modulo, che è un tipo di veicolo.
Supponiamo di aver risolto il problema di implementazione e di avere una logica aziendale stabile. La cosa più semplice da fare è fornire un'unica interfaccia con tutte le implementazioni e lasciare che i clienti, nel nostro caso Stazione degli autobus
, Autostrada
, autista
e così via, per usare qualsiasi cosa voglia dall'implementazione dell'interfaccia. Fondamentalmente, questo sposta la responsabilità della selezione del comportamento verso i clienti. Puoi trovare questo tipo di soluzione in molte vecchie applicazioni.
Il principio di separazione delle interfacce (ISP) afferma che nessun client dovrebbe essere costretto a dipendere da metodi che non usa.
Tuttavia, questa soluzione ha i suoi problemi. Ora tutti i client dipendono da tutti i metodi. Perché dovrebbe a Stazione degli autobus
dipende dallo stato delle luci del bus o dai canali radio selezionati dal conducente? Non dovrebbe. Ma cosa succede se lo fa? Importa? Bene, se pensiamo al Principio di Responsabilità Unica, è un concetto gemello per questo. Se Stazione degli autobus
dipende da molte implementazioni individuali, nemmeno utilizzate da esso, potrebbe richiedere modifiche se una qualsiasi delle piccole implementazioni individuali cambia. Questo è particolarmente vero per le lingue compilate, ma possiamo ancora vedere l'effetto del Controllo della luce
cambiare impatto Stazione degli autobus
. Queste cose non dovrebbero mai accadere.
Le interfacce appartengono ai loro clienti e non alle implementazioni. Pertanto, dovremmo sempre progettarli in modo da soddisfare al meglio i nostri clienti. Alcune volte possiamo, a volte non possiamo conoscere esattamente i nostri clienti. Ma quando possiamo, dovremmo rompere le nostre interfacce in molte più piccole, in modo da soddisfare meglio le esigenze esatte dei nostri clienti.
Naturalmente, questo porterà ad un certo grado di duplicazione. Ma ricorda! Le interfacce sono semplici definizioni di nomi di funzioni. Non c'è implementazione di alcun tipo di logica in loro. Quindi le duplicazioni sono piccole e gestibili.
Quindi, abbiamo il grande vantaggio dei clienti che dipendono solo e solo da ciò che effettivamente hanno bisogno e usano. In alcuni casi, i client possono usare e necessitano di diverse interfacce, va bene, a patto che usino tutti i metodi da tutte le interfacce da cui dipendono.
Un altro bel trucco è che nella nostra logica aziendale, una singola classe può implementare diverse interfacce, se necessario. Quindi possiamo fornire una singola implementazione per tutti i metodi comuni tra le interfacce. Le interfacce segregate ci obbligheranno anche a pensare al nostro codice più dal punto di vista del cliente, che a sua volta porterà a un accoppiamento lento ea test facili. Quindi, non solo abbiamo reso il nostro codice migliore per i nostri clienti, ma abbiamo anche reso più semplice comprendere, testare e implementare.
LSP ci ha insegnato perché la realtà non può essere rappresentata come una relazione uno-a-uno con oggetti programmati e come i sottotipi dovrebbero rispettare i loro genitori. Lo mettiamo anche alla luce degli altri principi che già sapevamo.
ISP ci insegna a rispettare i nostri clienti più di quanto pensassimo necessario. Rispettare i loro bisogni renderà il nostro codice migliore e le nostre vite come programmatori più facili.
Grazie per il tuo tempo.