Refactoring Legacy Code Part 8 - Inversione delle dipendenze per un'architettura pulita

Vecchio codice Codice brutto. Codice complicato Codice degli spaghetti Assurdità spudorate. In due parole, Codice legacy. Questa è una serie che ti aiuterà a lavorare e ad affrontarla.

È giunto il momento di parlare di architettura e di come organizziamo i nostri nuovi livelli di codice. È tempo di prendere la nostra applicazione e provare a mapparla alla progettazione architettonica teorica.

Architettura pulita

Questo è qualcosa che abbiamo visto nei nostri articoli e tutorial. Architettura pulita.

Ad un livello elevato, sembra lo schema sopra e sono sicuro che lo conosci già. È, una soluzione architettonica proposta da Robert C. Martin.

Al centro della nostra architettura c'è la nostra logica di business. Queste sono le classi che rappresentano i processi aziendali che la nostra applicazione cerca di risolvere. Queste sono le entità e le interazioni che rappresentano il dominio del nostro problema.

Quindi, ci sono molti altri tipi di moduli o classi attorno alla nostra logica aziendale. Questi possono essere visti come semplici moduli ausiliari. Hanno vari scopi e molti di loro sono indispensabili. Forniscono la connessione tra l'utente e la nostra applicazione attraverso un meccanismo di consegna. Nel nostro caso, questa è un'interfaccia a riga di comando. C'è un altro insieme di classi ausiliarie che collegano la nostra logica aziendale al nostro livello di persistenza e a tutti i dati in quel livello, ma non abbiamo un tale livello nella nostra applicazione. Poi ci sono le classi che aiutano come fabbriche e costruttori che stanno costruendo e fornendo nuovi oggetti alla nostra logica di business. Infine ci sono le classi che rappresentano il punto di ingresso al nostro sistema. Nel nostro caso, GameRunner può essere considerato come una classe, o tutti i nostri test sono anche punti di ingresso a modo loro.

La cosa più importante da notare sul diagramma è la direzione della dipendenza. Tutte le classi ausiliarie dipendono dalla logica aziendale. La logica aziendale non dipende da nient'altro. Se tutti gli oggetti nella nostra logica aziendale potessero apparire magicamente, con tutti i dati al loro interno, e potessimo vedere direttamente ciò che accade all'interno del nostro computer, dovrebbero essere in grado di funzionare. La nostra logica aziendale deve essere in grado di funzionare senza un'interfaccia utente o senza un livello di persistenza. La nostra logica aziendale deve esistere isolata, in una bolla di un universo logico.

Il principio di inversione delle dipendenze

A. I moduli di alto livello non dovrebbero dipendere da moduli di basso livello. Entrambi dovrebbero dipendere dalle astrazioni.
B. Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni.

Questo è, l'ultimo principio SOLID e probabilmente quello con il più grande effetto sul tuo codice. È semplice da capire e molto semplice da implementare.

In termini semplici, dice che le cose concrete dovrebbero sempre dipendere da cose astratte. Il tuo database è molto concreto, quindi dovrebbe dipendere da qualcosa di più astratto. La tua interfaccia utente è molto concreta, quindi dovrebbe dipendere da qualcosa di più astratto. Le tue fabbriche sono di nuovo molto concrete. Ma che dire della tua logica aziendale. All'interno della tua logica aziendale dovresti continuare ad applicare queste idee, in modo che le classi più vicine ai confini dipendano da classi più astratte, più al centro della tua logica aziendale.

Una pura logica aziendale, rappresenta in modo astratto i processi e i comportamenti di un dominio o di un modello aziendale definiti. Tale logica aziendale non contiene elementi specifici (cose concrete) come valori, denaro, nomi di account, password, la dimensione di un pulsante o il numero di campi in un modulo. La logica del business non dovrebbe interessare a cose concrete. Dovrebbe preoccuparsi solo dei tuoi processi aziendali.

Il trucco tecnico

Quindi, il principio di inversione di dipendenza (DIP) dice che dovremmo invertire le nostre dipendenze ogni volta che c'è un codice che dipende da qualcosa di concreto. In questo momento la nostra struttura di dipendenza appare così.

GameRunner, utilizzando le funzioni in RunnerFunctions.php sta creando a Gioco classe e quindi lo usa. D'altra parte, il nostro Gioco classe, che rappresenta la nostra logica aziendale, crea e utilizza a Display oggetto.

Quindi, il corridore dipende dalla nostra logica aziendale. È corretto. D'altra parte, il nostro Gioco dipende da Display, che non va bene. La nostra logica di business non dovrebbe mai dipendere dalla nostra presentazione.

Il trucco tecnico più semplice che possiamo fare è usare i costrutti astratti nel nostro linguaggio di programmazione. Una classe tradizionale è più concreta di una classe astratta, che è più concreta di un'interfaccia.

Un Classe astratta è un tipo speciale che non può essere inizializzato. Contiene solo definizioni e implementazioni parziali. Una classe base astratta di solito ha diverse classi figlio. Queste classi figlie ereditano la funzionalità parziale comune dal genitore astratto, aggiungono il proprio comportamento esteso e devono implementare tutti i metodi definiti nel genitore astratto ma non implementati in esso.

Un Interfaccia è un tipo speciale che consente solo la definizione di metodi e variabili. È il più astratto costrutto nella programmazione orientata agli oggetti. Qualsiasi implementazione deve sempre implementare tutti i metodi della sua interfaccia genitore. Una classe concreta può implementare diverse interfacce.

Ad eccezione dei linguaggi orientati agli oggetti della famiglia C, gli altri come Java o PHP non consentono l'ereditarietà multipla. Quindi una classe concreta può estendere una singola classe astratta ma può implementare diverse interfacce, anche se allo stesso tempo se necessario. O da un'altra prospettiva, una singola classe astratta può avere molte implementazioni, mentre molte interfacce possono avere molte implementazioni.

Per una spiegazione più completa del DIP, leggere il tutorial dedicato a questo principio SOLID.

Inversione della dipendenza mediante un'interfaccia

PHP supporta pienamente le interfacce. A partire dal Display class come il nostro modello, potremmo definire un'interfaccia con i metodi pubblici che tutte le classi responsabili della visualizzazione dei dati dovranno implementare.

Guardando DisplayL'elenco dei metodi, ci sono 12 metodi pubblici, incluso il costruttore. Questa è un'interfaccia abbastanza grande, dovresti mantenere questo numero il più basso possibile, esponendo le interfacce di cui i clienti hanno bisogno. L'Interfaccia Segregation Principle ha alcune buone idee a riguardo. Forse proveremo ad affrontare questo problema in un futuro tutorial.

Quello che vogliamo ottenere ora è un'architettura come quella qui sotto.

In questo modo, invece di Gioco a seconda del più concreto Display, entrambi dipendono dall'interfaccia molto astratta. Gioco usa l'interfaccia, mentre Display lo implementa.

Naming Interfaces

Phil Karlton ha dichiarato: "Ci sono solo due cose difficili in Computer Science: invalidazione della cache e denominazione delle cose".

Mentre non ci interessa la cache, dobbiamo nominare le nostre classi, variabili e metodi. Le interfacce di denominazione possono essere una vera sfida.

Ai vecchi tempi della notazione ungherese, lo avremmo fatto in questo modo.

Per questo diagramma, abbiamo utilizzato i nomi di classe / file effettivi e l'effettiva maiuscola. L'interfaccia è chiamata "IDisplay" con una "I" maiuscola davanti a "Display". In realtà esistevano linguaggi di programmazione che richiedevano tale denominazione per le interfacce. Sono sicuro che ci sono ancora alcuni lettori che li usano e stanno sorridendo in questo momento.

Il problema con questo schema di denominazione è la preoccupazione malriposta. Le interfacce appartengono ai loro clienti. La nostra interfaccia appartiene a Gioco. così Gioco non deve sapere che usa un'interfaccia o un oggetto reale. Gioco non deve preoccuparsi dell'implementazione che effettivamente ottiene. A partire dal GiocoDal punto di vista, utilizza solo un "Display", tutto qui.

Questo risolve il Gioco a Display problema di denominazione. L'utilizzo del suffisso "Impl" per l'implementazione è in qualche modo migliore. Aiuta a eliminare la preoccupazione da Gioco.

È anche molto più efficace per noi. Pensa a Gioco come sembra adesso. Usa a Display oggetto e sa come usarlo. Se nominiamo la nostra interfaccia "Display", ridurremo il numero di modifiche necessarie in Gioco.

Tuttavia, questa denominazione è leggermente migliore della precedente. Permette solo una implementazione per Display e il nome dell'implementazione non ci dirà di che tipo di display stiamo parlando.

Ora è molto meglio. La nostra implementazione è stata denominata "CLIDisplay", in quanto viene inviata alla CLI. Se vogliamo un output HTML o un'interfaccia utente desktop di Windows, possiamo facilmente aggiungere tutto ciò alla nostra architettura.

Mostrami il codice

Dato che abbiamo due tipi di test, il maestro d'oro lento e i test di unità veloci, vogliamo fare affidamento sui test unitari il più possibile, e su golden master il meno possibile. Quindi segniamo i nostri golden master test come saltati e proviamo a fare affidamento sui nostri test unitari. Stanno passando in questo momento e vogliamo fare un cambiamento che li manterrà al loro passaggio. Ma come possiamo fare una cosa del genere, senza fare tutte le modifiche proposte sopra?

C'è un modo di test che ci consenta di fare un passo più piccolo?

La beffa salva il giorno

C'è un modo Nel test, c'è un concetto chiamato "Mocking".

Wikipedia definisce Mocking come tale, "Nella programmazione orientata agli oggetti, gli oggetti fittizi sono oggetti simulati che imitano il comportamento degli oggetti reali in modi controllati."

Un tale oggetto sarebbe di grande aiuto per noi. In realtà, non abbiamo nemmeno bisogno di qualcosa di così complesso come simulare tutto il comportamento. Tutto ciò di cui abbiamo bisogno è un oggetto falso e stupido su cui possiamo inviare Gioco invece della vera logica di visualizzazione.

Creare l'interfaccia

Creiamo un'interfaccia chiamata Display con tutti i metodi pubblici dell'attuale classe concreta.

Come puoi osservare, il vecchio Display.php è stato rinominato DisplayOld.php. Questo è solo un passaggio temporaneo, che ci consente di toglierlo di mezzo e concentrarci sull'interfaccia.

interfaccia Display  

Questo è tutto ciò che c'è da creare un'interfaccia. Puoi vedere che è definito come "interfaccia" e non come "classe". Aggiungiamo i metodi.

interfaccia Display function statusAfterRoll ($ rolledNumber, $ currentPlayer); function playerSentToPenaltyBox ($ currentPlayer); function playerStaysInPenaltyBox ($ currentPlayer); function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); function playerAdded ($ playerName, $ numberOfPlayers); funzione askQuestion ($ currentCategory); function correctAnswer (); function correctAnswerWithTypo (); function incorrectAnswer (); function playerCoins ($ currentPlayer, $ playerCoins);  

Sì. Un'interfaccia è solo un mucchio di dichiarazioni di funzioni. Immaginalo come un file di intestazione C. Nessuna implementazione, solo dichiarazioni. Non può assolutamente sostenere un'implementazione. Se si tenta di implementare uno qualsiasi dei metodi, si verificherà un errore.

Ma queste definizioni molto astratte ci permettono qualcosa di meraviglioso. Nostro Gioco la classe ora dipende da loro, invece di un'implementazione concreta. Tuttavia, se proviamo a eseguire i nostri test, falliranno.

Errore irreversibile: impossibile istanziare la visualizzazione dell'interfaccia

Questo perchè Gioco prova a creare un nuovo display autonomamente alla riga 25, nel costruttore.

Sappiamo che non possiamo farlo. Un'interfaccia o una classe astratta non possono essere istanziate. Abbiamo bisogno di un vero oggetto.

Iniezione di dipendenza

Abbiamo bisogno di un oggetto fittizio da utilizzare nei nostri test. Una classe semplice, che implementa tutti i metodi del Display interfaccia, ma non fare nulla. Scriviamolo direttamente all'interno del nostro test unitario. Se il tuo linguaggio di programmazione non consente più classi nello stesso file, sentiti libero di creare un nuovo file per la tua classe dummy.

classe DummyDisplay implementa Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementa il metodo statusAfterRoll ().  function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementa il metodo playerSentToPenaltyBox ().  function playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementa il metodo playerStaysInPenaltyBox ().  function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementa il metodo statusAfterNonPenalizedPlayerMove ().  function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementa il metodo statusAfterPlayerGettingOutOfPenaltyBox ().  function playerAdded ($ playerName, $ numberOfPlayers) // TODO: Implementa il metodo playerAdded ().  function askQuestion ($ currentCategory) // TODO: Implementa il metodo askQuestion ().  function correctAnswer () // TODO: Implementa il metodo correctAnswer ().  function correctAnswerWithTypo () // TODO: Implementa il metodo correctAnswerWithTypo ().  function incorrectAnswer () // TODO: Implementa il metodo incorrectAnswer ().  function playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implementa il metodo playerCoins (). 

Non appena dici che la tua classe implementa un'interfaccia, l'IDE ti permetterà di compilare automaticamente i metodi mancanti. Questo rende la creazione di tali oggetti molto veloce, in pochi secondi.

Ora usiamolo dentro Gioco inizializzandolo nel suo costruttore.

function __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = new DummyDisplay (); 

Questo fa passare il test, ma introduce un enorme problema. Gioco deve sapere del suo test. Non vogliamo davvero questo. Un test è solo un altro punto di accesso. Il DummyDisplay è solo un'altra interfaccia utente. La nostra logica aziendale, il Gioco classe, non dovrebbe dipendere dall'interfaccia utente. Quindi facciamo in modo che dipendano solo dall'interfaccia.

function __construct (Display $ display) $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = $ display; 

Ma per testare Gioco, dobbiamo inviare la visualizzazione fittizia dai nostri test.

function setUp () $ this-> game = new Game (nuovo DummyDisplay ()); 

Questo è tutto. Dovevamo modificare una singola riga nei nostri test unitari. Nel setup, invieremo, come parametro, una nuova istanza di DummyDisplay. Questa è un'iniezione di dipendenza. L'utilizzo di interfacce e di dipendenze aiuta soprattutto se si lavora in un team. Noi di Syneto abbiamo osservato che specificare un tipo di interfaccia per una classe e iniettarla, ci aiuterà a comunicare molto meglio le intenzioni del codice cliente. Chiunque guardi il cliente saprà quale tipo di oggetto viene utilizzato nei parametri. E un bonus interessante è che l'IDE completerà automaticamente i metodi per questi parametri perché può determinare i loro tipi.

Una Real Implementation per Golden Master

Il golden master test, esegue il nostro codice come nel mondo reale. Per farlo passare, abbiamo bisogno di trasformare la nostra vecchia classe di display in un'implementazione reale dell'interfaccia e di inviarla nella nostra logica di business. Ecco un modo per farlo.

classe CLIDisplay implementa Display // ... //

Rinominalo in CLIDisplay e fallo implementare Display.

function run () $ display = new CLIDisplay (); $ aGame = nuovo gioco ($ display); $ AGame-> aggiungere ( "Chet"); $ AGame-> aggiungere ( "Pat"); $ AGame-> aggiungere ( "Sue"); do $ dice = rand (0, 5) + 1; $ AGame-> rotolo ($ dadi);  while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ())); 

Nel RunnerFunctions.php, nel correre() funzione, creare un nuovo display per la CLI e passarlo a Gioco quando viene creato.

Rimuovi il commento e esegui i tuoi golden master test. Passeranno.

Pensieri finali

Questa soluzione porta in effetti a un'architettura come nel diagramma sottostante.

Quindi ora il nostro game runner, che è il punto di partenza per la nostra applicazione, crea un concreto CLIDisplay e quindi dipende da questo. CLIDisplay dipende solo dall'interfaccia che si trova sul confine tra la presentazione e la logica aziendale. Il nostro corridore dipende anche direttamente dalla logica di business. Ecco come appare la nostra applicazione quando proiettata sull'architettura pulita con cui abbiamo iniziato questo articolo.

Grazie per la lettura, e non perdere il prossimo tutorial quando parleremo di simulazione e interazione di classe in maggiori dettagli.