Refactoring Legacy Code Part 7 - Identifying the Presentation Layer

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.

In questo settimo capitolo dei nostri tutorial di refactoring, faremo un diverso tipo di refactoring. Abbiamo osservato nelle lezioni precedenti che esiste un codice relativo alla presentazione sparsi su tutto il nostro codice legacy. Cercheremo di identificare tutto il codice relativo alla presentazione che possiamo e prenderemo quindi i provvedimenti necessari per separarlo dalla logica aziendale.

La forza motrice

Ogni volta che eseguiamo un cambiamento di refactoring sul nostro codice, lo facciamo seguendo alcuni principi. Questi principi e queste regole ci aiutano a identificare i problemi e in molti casi ci stanno indirizzando nella giusta direzione per rendere il codice migliore.

Il principio di responsabilità unica (SRP)

SRP è uno dei principi SOLID di cui abbiamo parlato in modo molto dettagliato in un precedente tutorial: SOLID: Parte 1 - Il principio della singola responsabilità. Se vuoi approfondire i dettagli, ti consiglio di leggere l'articolo, altrimenti continua a leggere e vedi un riepilogo del Principio di Responsabilità Unica sotto.

SRP dice fondamentalmente che qualsiasi modulo, classe o metodo dovrebbe avere una singola responsabilità. Tale responsabilità è definita come un asse di cambiamento. Un asse di cambiamento è una direzione, una ragione per cambiare. Quindi, SRP significa che la nostra classe dovrebbe avere una sola ragione per cambiare.

Mentre questo suona piuttosto semplice, come definisci una "ragione del cambiamento"? Dobbiamo pensarci dal punto di vista degli utenti del nostro codice, sia per gli utenti ordinari che per i vari dipartimenti software. Questi utenti possono essere rappresentati come attori. Quando un attore vuole che cambiamo il nostro codice, questo è un motivo di cambiamento che determina un asse di cambiamento. Tale richiesta dovrebbe interessare solo uno dei nostri moduli, classi o metodi, se possibile.

Un esempio molto ovvio sarebbe se il nostro team di progettazione dell'interfaccia utente ci chiedesse di fornire tutte le informazioni che devono essere presentate in modo che la nostra applicazione possa essere distribuita su una pagina Web HTML, invece della nostra attuale interfaccia a riga di comando.

Dato che il nostro codice è valido oggi, potremmo semplicemente inviare tutto il testo a qualche oggetto smart esterno che lo trasformerebbe in HTML. Ma questo può funzionare solo perché l'HTML è principalmente basato sul testo. Che cosa succede se il nostro team di interfaccia utente vuole presentare il nostro gioco a quiz come interfaccia utente desktop, con finestre, pulsanti e vari tavoli?

Cosa succede se i nostri utenti vogliono vedere il gioco su una scacchiera virtuale rappresentata come una città con le strade, ei giocatori come persone che camminano intorno al blocco?

Potremmo identificare queste persone come l'attore dell'IU. E dobbiamo renderci conto che, come il nostro codice si trova oggi, avremmo bisogno di modificare la nostra lezione di curiosità e quasi tutti i suoi metodi. Sembra logico modificare il wasCorrectlyAnswered () metodo dal Gioco classe se voglio correggere un errore di battitura sullo schermo in un testo, o se voglio presentare il nostro trivia come un tabellone virtuale? No. La risposta è assolutamente no.

Architettura pulita

Clean Architecture è un concetto promosso principalmente da Robert C. Martin. Fondamentalmente dice che la nostra logica di business dovrebbe essere ben definita e chiaramente separata dai confini di altri moduli non correlati alle funzionalità di base del nostro sistema. Questo porta al codice disaccoppiato e altamente testabile.

Potresti aver visto questo disegno durante i miei tutorial e corsi. Lo considero così importante, che non scrivo mai codice o parlo di codice senza pensarci. Ha completamente cambiato il modo in cui scriviamo il codice in Syneto e come appare il nostro progetto. Prima avevamo tutto il nostro codice in un framework MVC, con la logica aziendale nei Modelli. Era difficile da capire e difficile da testare. Inoltre, la logica aziendale era totalmente accoppiata a quella specifica struttura MVC. Mentre ciò potrebbe funzionare con piccoli progetti di animali domestici, quando si tratta di un grande progetto da cui dipende il futuro di un'azienda, inclusi tutti i suoi dipendenti in un certo senso, è necessario smettere di giocare con i framework MVC e si deve iniziare a pensare a come organizzare il tuo codice. Una volta che lo fai, e fallo bene, non vorrai mai tornare ai modi in cui hai architettato i tuoi progetti.

Osservando l'appartenenza

Abbiamo già iniziato a separare la nostra logica aziendale dalla presentazione nei pochi tutorial precedenti. A volte abbiamo osservato alcune funzioni di stampa e le abbiamo estratte in metodi privati ​​nella stessa Gioco classe. Questa era la nostra mente inconscia che ci diceva di spingere la presentazione fuori dalla logica del business a livello di metodo.

Ora è il momento di analizzare e osservare.

Questa è la lista di tutte le variabili, i metodi e le funzioni del nostro Game.php file. Le cose contrassegnate da una "f" arancione sono variabili. La "m" rossa indica il metodo. Se è seguito da un lucchetto verde, è pubblico. È seguito dal blocco rosso è privato. E da quella lista, tutto ciò a cui siamo interessati, è la seguente parte.

Tutti i metodi selezionati hanno qualcosa in comune. Tutti i loro nomi iniziano con "display" ... qualcosa. Sono tutti metodi correlati alla stampa di cose sullo schermo. Sono stati tutti identificati da noi in tutorial precedenti e estratti senza problemi, uno alla volta. Ora dobbiamo osservare che sono un gruppo di metodi che appartengono insieme. Un gruppo che fa una cosa specifica, soddisfa una singola responsabilità, visualizza le informazioni sullo schermo.

Il refactoring della classe Extract

Meglio esemplificato e spiegato in Refactoring - Migliorare il design del codice esistente di Martin Fowler, l'idea di base del refactoring della classe Extract è che dopo aver realizzato che la tua classe funziona e che dovrebbe essere eseguita in due classi, devi intraprendere azioni per due classi. Ci sono meccanismi specifici a questo, come spiegato nella citazione qui sotto dal libro sopra menzionato.

  • Decidi come dividere le responsabilità della classe.
  • Crea una nuova classe per esprimere le responsabilità scisse.
    • Se le responsabilità della vecchia classe non corrispondono più al suo nome, rinomina la vecchia classe.
  • Crea un collegamento dalla vecchia alla nuova classe.
    • Potrebbe essere necessario un collegamento a due vie. Ma non creare il back link finché non trovi di averne bisogno.
  • Usa il campo Sposta su ogni campo che desideri spostare.
  • Compila e prova dopo ogni mossa.
  • Usa il metodo Sposta per spostare i metodi dal vecchio al nuovo. Inizia con i metodi di livello inferiore (chiamati piuttosto che chiamare) e crea il livello più alto.
  • Compila e prova dopo ogni mossa.
  • Riesaminare e ridurre le interfacce di ogni classe.
    • Se hai un link a due vie, esamina per vedere se può essere trasformato in un modo.
  • Decidi se esporre la nuova classe. Se esporti la classe, decidi se esporla come oggetto di riferimento o come oggetto valore immutabile.

Applicazione della classe Extract

Sfortunatamente al momento di scrivere questo articolo, non c'è IDE in PHP che possa fare una classe di estrazione semplicemente selezionando un gruppo di metodi e applicando un'opzione dal menu.

Poiché non fa mai male conoscere la meccanica dei processi che implicano il lavoro con il codice, eseguiremo i passaggi precedenti, uno per uno e li applicheremo al nostro codice.

Decidere come dividere le responsabilità

Lo sappiamo già. Vogliamo interrompere la presentazione dalla logica aziendale. Vogliamo dare output, mostrare funzioni e altro codice e spostarli da qualche altra parte.

Crea una nuova classe

La nostra prima azione è creare una nuova classe vuota.

classe Display  

Sì. È tutto per ora. E anche trovare un nome adatto era abbastanza semplice. Display è la parola tutti i nostri metodi che ci interessano all'inizio. È il comune denominatore dei loro nomi. È un suggerimento molto potente sul loro comportamento comune, il comportamento dopo il quale abbiamo chiamato la nostra nuova classe.

Se preferisci e il linguaggio di programmazione lo supporta, PHP lo fa, puoi creare la nuova classe all'interno dello stesso file di quello vecchio. Oppure, puoi creare un nuovo file per esso dall'inizio. Personalmente non ho trovato alcuna ragione definitiva per andare in entrambi i modi o vietare nessuno dei due modi. Quindi dipende da te. Basta decidere e andare avanti.

Collegamento dalla vecchia classe alla nuova classe

Questo passaggio potrebbe non sembrare molto familiare. Significa dichiarare una variabile di classe nella vecchia classe e renderla un'istanza di quella nuova.

require_once __DIR__. '/Display.php'; function echoln ($ string) echo $ string. "\ N";  class Game static $ minimumNumberOfPlayers = 2; static $ numberOfCoinsToWin = 6; visualizzazione $ privata; // ... // function __construct () // ... // $ this-> display = new Display ();  // ... tutti gli altri metodi ... //

Semplice. Non è vero? Nel GiocoIl costruttore ha appena inizializzato una variabile di classe privata che abbiamo chiamato la stessa della nuova classe, display. Dovevamo anche includere il Display.php file nel nostro Game.php file. Non abbiamo ancora un autoloader. Forse in un futuro tutorial ne introdurremo uno se necessario.

E come al solito, non dimenticare di eseguire i test. I test unitari sono sufficienti in questa fase, solo per assicurarsi che non vi siano errori di battitura nel codice appena aggiunto.

Il campo Sposta e compila / prova

Prendiamo questi due passaggi contemporaneamente. Quali campi possiamo identificare da cui dovrebbe andare Gioco a Display?

Solo guardando la lista ...

static $ minimumNumberOfPlayers = 2; static $ numberOfCoinsToWin = 6; visualizzazione $ privata; var $ giocatori; var $ places; var $ borse; var $ inPenaltyBox; var $ popQuestions; var $ scienceQuestions; var $ sportsQuestions; var $ rockQuestions; var $ currentPlayer = 0; var $ isGettingOutOfPenaltyBox; 

... non possiamo trovare alcuna variabile / campo a cui deve appartenere Display. Forse alcuni emergeranno in tempo. Quindi niente da fare per questo passaggio. E riguardo ai test, li abbiamo già eseguiti un attimo fa. Ora di andare avanti.

Sposta i metodi nella nuova classe

Questo è di per sé, un altro refactoring. Puoi farlo in diversi modi e troverai una bella definizione nello stesso libro di cui abbiamo parlato prima.

Come accennato in precedenza, dovremmo iniziare con il livello più basso dei metodi. Quelli che non chiamano altri metodi. Invece sono chiamati.

funzione privata displayPlayersNewLocation () echoln ($ this-> players [$ this-> currentPlayer]. "La nuova posizione è". $ this-> places [$ this-> currentPlayer]); 

displayPlayersNewLocation () sembra essere un buon candidato. Analizziamo ciò che fa.

Possiamo vedere che non chiama altri metodi su Gioco. Invece, utilizza tre campi: Giocatori, currentPlayer, e posti. Quelli possono trasformarsi in due o tre parametri. Fin qui molto carino. Ma per quanto riguarda echoln (), l'unica chiamata di funzione nel nostro metodo? Dov'è questo echoln () proveniente da?

È in cima alla nostra Game.php file, al di fuori del Gioco classe stessa.

function echoln ($ string) echo $ string. "\ N"; 

Fa sicuramente quello che dice. Elimina una stringa con una nuova riga alla fine. E questa è pura presentazione. Dovrebbe andare nel Display classe. Quindi copiamolo laggiù.

class Display function echoln ($ string) echo $ string. "\ N"; 

Esegui di nuovo i nostri test. Possiamo mantenere disattivato il master dorato fino a quando non abbiamo finito di estrarre tutta la presentazione al nuovo Display classe. In qualsiasi momento, se ritieni che l'output possa essere stato modificato, riesegui anche i golden master test. A questo punto, i test attesteranno che non abbiamo introdotto errori di battitura o dichiarazioni di funzioni duplicate o altri errori del caso, copiando la funzione nel suo nuovo posto.

Ora vai e cancella echoln () dal Game.php file, esegui i nostri test e aspettati che falliscano.

PHP Errore irreversibile: chiamata alla funzione non definita echoln () in / ... /Game.php alla riga 55

Bello! Il nostro test unitario è di grande aiuto qui. Funziona molto velocemente e ci dice la posizione esatta del problema. Andiamo alla linea 55.

Guarda! C'è un echoln () chiama lì. I test non mentono mai. Risolviamolo chiamando $ This-> dipslay-> echoln () anziché.

funzione add ($ playerName) array_push ($ this-> players, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "è stato aggiunto"); echoln ("Sono il numero del giocatore". count ($ this-> players)); ritorna vero; 

Ciò fa passare il test attraverso la linea 55 e fallisce su 56.

PHP Errore irreversibile: chiamata alla funzione non definita echoln () in / ... /Game.php alla riga 56

E la soluzione è ovvia. Questo è un processo noioso, ma è almeno facile.

funzione add ($ playerName) array_push ($ this-> players, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "è stato aggiunto"); $ this-> display-> echoln ("They are player number". count ($ this-> players)); ritorna vero; 

Questo fa passare i primi tre test e ci dice anche il prossimo luogo dove c'è una chiamata che dovremmo cambiare.

PHP Errore irreversibile: chiamata alla funzione non definita echoln () in / ... /Game.php sulla riga 169

Questo è dentro risposta sbagliata().

function wrongAnswer () echoln ("La domanda non ha risposto correttamente"); echoln ($ this-> players [$ this-> currentPlayer]. "è stato inviato nella casella di penalizzazione"); $ this-> inPenaltyBox [$ this-> currentPlayer] = true; $ This-> currentPlayer ++; if ($ this-> shouldResetCurrentPlayer ()) $ this-> currentPlayer = 0;  return true; 

Risolvere queste due chiamate, spinge il nostro errore fino alla riga 228.

funzione privata displayCurrentPlayer () echoln ($ this-> players [$ this-> currentPlayer]. "è il giocatore attuale"); 

UN display metodo! Forse questo dovrebbe essere il nostro primo metodo di movimento. Proviamo a fare un piccolo sviluppo guidato da test (TDD) qui. E quando i test falliscono, non ci è permesso scrivere altro codice di produzione che non è assolutamente necessario per fare passare il test. E tutto ciò che comporta è solo cambiando il echoln () chiama finché non passano tutti i nostri test unitari.

È possibile velocizzare questo processo utilizzando la funzionalità di ricerca e sostituzione dell'IDE o dell'editor. Esegui tutti i test, incluso il master dorato dopo aver terminato questa sostituzione. I nostri test unitari non coprono tutto il codice e tutto il echoln () chiamate.

Possiamo iniziare senza il primo candidato, displayCurrentPlayer (). Copia su Display e fai i tuoi test.

Quindi, rendilo pubblico Display e in displayCurrentPlayer () nel Gioco chiamata $ This-> Display-> displayCurrentPlayer () invece di fare direttamente un echoln (). Finalmente, esegui i tuoi test.

Falliranno. Ma facendo il cambiamento in questo modo, ci siamo assicurati che abbiamo cambiato solo una cosa che poteva fallire. Tutti gli altri metodi stanno ancora chiamando Gioco'S displayCurrentPlayer (). E questo è delegato a Display.

 Proprietà non definita: Visualizza :: $ display

Il nostro metodo utilizza i campi classe. Questi devono essere fatti parametri per la funzione. Se segui i tuoi errori di test, dovresti finire con qualcosa di simile in Gioco.

funzione privata displayCurrentPlayer () $ this-> display-> displayCurrentPlayer ($ this-> players [$ this-> currentPlayer]); 

E questo dentro Display.

function displayCurrentPlayer ($ currentPlayer) $ this-> echoln ($ currentPlayer. "è il giocatore corrente"); 

Sostituisci chiamate in Gioco al metodo locale con quello in Display. Non dimenticare di spostare i parametri di un livello, anche.

funzione privata displayStatusAfterRoll ($ rolledNumber) $ this-> display-> displayCurrentPlayer ($ this-> players [$ this-> currentPlayer]); $ This-> displayRolledNumber ($ rolledNumber); 

Infine, rimuovi il metodo non usato da Gioco. E fai i tuoi test per assicurarti che tutto sia OK.

Questo è un processo noioso. Puoi accelerarlo un po 'adottando diversi metodi contemporaneamente e usando tutto ciò che l'IDE può fare per aiutare a spostare e sostituire il codice tra le classi. Il resto dei metodi rimarrà un esercizio per te o puoi leggere più questo capitolo con i punti salienti del processo. Il codice completo allegato a questo articolo conterrà il completo Display classe.

Ah, e non dimenticare il codice che non è ancora stato estratto nei metodi di "visualizzazione" all'interno Gioco. Puoi spostarli echoln () chiama per visualizzare direttamente. Il nostro obiettivo è non chiamare echoln () affatto da Gioco, e renderlo privato Display.

Dopo solo mezz'ora circa di lavoro, Display inizia a sembrare carino.

Tutti i metodi di visualizzazione da Gioco sono dentro Display. Ora possiamo cercare tutto echoln chiama che è rimasto in Gioco e spostali anche loro. I test stanno passando, ovviamente.

Ma non appena ci troviamo di fronte al askQuestion () metodo, ci rendiamo conto che è solo il codice di presentazione. E ciò significa che anche i vari array di domande dovrebbero andare Display.

class Display private $ popQuestions = []; private $ scienceQuestions = []; private $ sportsQuestions = []; private $ rockQuestions = []; function __construct () $ this-> initializeQuestions ();  // ... // private function initializeQuestions () $ categorySize = 50; per ($ i = 0; $ i < $categorySize; $i++)  array_push($this->popQuestions, "Pop Question". $ I); array_push ($ this-> scienceQuestions, ("Science Question". $ i)); array_push ($ this-> sportsQuestions, ("Domande sportive". $ i)); array_push ($ this-> rockQuestions, "Rock Question". $ i); 

Sembra giusto. Le domande sono solo stringhe, le presentiamo e si adattano meglio qui. Quando facciamo questo tipo di refactoring, è anche una buona opportunità per rifattorizzare il codice appena trasferito. Abbiamo definito i valori iniziali nella dichiarazione dei campi, li abbiamo anche resi privati ​​e abbiamo creato un metodo con il codice che deve essere eseguito in modo che non si tratti solo del costruttore. Invece, è nascosto nella parte inferiore della classe, fuori strada.

Dopo aver estratto i prossimi due metodi, ci rendiamo conto che è più bello nominarli, all'interno di Display classe, senza il prefisso "display".

function correctAnswer () $ this-> echoln ("La risposta era corretta !!!!");  function playerCoins ($ currentPlayer, $ playerCoins) $ this-> echoln ($ currentPlayer. "ora ha". $ playerCoins. "Gold Coins."); 

Con i nostri test verdi e facendo bene, ora possiamo refactoring e rinominare i nostri metodi. PHPStorm può gestire abbastanza bene i refactoring dei rinominati. Rinominerà le chiamate di funzione in Gioco di conseguenza. Poi c'è questo pezzo di codice.

Guarda attentamente la linea selezionata, 119. Sembra proprio come il nostro metodo recentemente estratto in Display.

function correctAnswer () $ this-> echoln ("La risposta era corretta !!!!"); 

Ma se lo chiamiamo al posto del codice, il test fallirà. Sì! C'è un errore di battitura. E NO! Non dovresti aggiustarlo. Stiamo refactoring. Dobbiamo mantenere inalterate le funzionalità, anche se c'è un bug.

Il resto del metodo non rappresenta una sfida speciale.

Riesaminare e ridurre le interfacce

Ora che tutte le funzionalità di presentazione sono in Display, dobbiamo rivedere i metodi e mantenere pubblici solo quelli utilizzati in Gioco. Questo passaggio è anche motivato dall'Inter Segregation Principle di cui abbiamo parlato in un precedente tutorial.

Nel nostro caso, il modo più semplice per capire quali metodi devono essere pubblici o privati ​​è semplicemente renderli privati ​​per volta, eseguire i test e, se falliscono, tornare pubblici.

Poiché i master test dorati sono lenti, possiamo anche fare affidamento sul nostro IDE per aiutarci ad accelerare il processo. PHPStorm è abbastanza intelligente da capire se un metodo non è utilizzato. Se rendiamo un metodo privato, e all'improvviso diventa inutilizzato, è chiaro che è stato usato al di fuori di Display e deve rimanere pubblico.

Finalmente, possiamo riorganizzarci Display in modo che i metodi privati ​​siano alla fine della classe.

Pensieri finali

Ora, l'ultimo passaggio del principio di refactoring della classe Extract è irrilevante nel nostro caso. Quindi, con questo, questo conclude il tutorial, ma questo non ha ancora concluso la serie. Restate sintonizzati per il nostro prossimo articolo in cui lavoreremo ulteriormente verso un'architettura pulita e invertiremo le dipendenze.