E dopo anni di silenzio, qualcosa da scrivere ogni tanto torna in mente...
Questa settimana abbiamo iniziato un nuovo progetto con il nostro team in XPeppers, l'azienda con cui collaboro da diversi anni.
La prima settimana l'abbiamo dedicata allo studio, approfondendo vari design pattern e tecniche per la programmazione orientata agli oggetti.
Un esercizio (ormai quasi un classico :P ) proposto dal nostro Coach, Matteo Vaccari, è di implementare il gioco del FizzBuzz. Brevemente, un'applicazione che dato un numero restituisce lo stesso numero; per multipli di certi numeri però sostituisce la stampa del numero con quella di determinate parole preimpostate.
Un esempio è che per multipli di due restituisce "Fizz", per multipli di tre "Buzz". Il numero "6" quindi restituirà la concatenazione, ovvero "FizzBuzz".
Chiaramente un'implementazione molto semplice potrebbe essere quella di creare una serie di blocchi IF per gestire i casi particolari.
Ovviamente non è scalabile, perchè all'aggiunta di nuove regole la serie di blocchi condizionali verrebbe a crescere in maniera sproporzionatamente complessa.
La sfida era quindi di scrivere con un design il più orientato ad oggetti possibile un algoritmo tutto sommato non semplicissimo (ad esempio, l'output è condizionato dalle regole precedenti).
Ecco la mia soluzione.
Il punto d'ingresso è game.java, che prende in costruzione tre oggetti: RulesBuilder, una factory che crea le regole con le giuste dipendenze, OutputBuilder, che si occupa di comporre i risultati delle regole (che sono a loro volta oggetti di tipo OutputAdder e non semplici stringhe) dando loro uno StringBuilder su cui scrivere del testo, e OutputPrinter, un'interfaccia di rendering su cui OutputBuilder stamperà la stringa risultante.
Le regole sono verificate dalla classe DividerRule; in questo caso si tratta di una classe concreta e ben specializzata; aggiungendo nuovi tipi di regole si potrebbe estrarre un'interfaccia comune.
Da notare che DividerRule ha la sola responsabilità di verificare l'applicarsi della condizione, ma non di scrivere o creare l'output; questa responsabilità è delegata alle singole implementazioni di OutputAdder.
Ad esempio, la DividerRule che deve controllare la divisibilità per 3 collaborerà con un AddToWordOutput istanziato per scrivere "Buzz". Lo stampare il numero senza sostituzioni è delegato a un caso particolare di OutputAdder: la DividerRule corrispondente viene sempre verificata, dato che viene istanziata per dividere per 1, mentre l'OutputAdder AddNumberIfOutputIsEmpty controlla che lo StringBuilder sia vuoto (ossia che le regole precedenti non hanno avuto successo) e in tal caso aggiunge il numero.
Altro caso particolare la divisibilità per 7: in questo caso PatatracAdder cancella tutto l'output delle regole precedenti e sostituisce il tutto con "Patatrac".
Il progetto così realizzato cerca di rispettare tutte le principali regole di buona programmazione: anzitutto SRP, dato che ogni classe è altamente specializzata ed ha una sola responsabilità; OCP, infatti per aggiungere nuove regole non serve modificare il codice esistente (a meno di stravolgimenti logici, ovviamente), ma è sufficiente implementare nuovi OutputAdder o eventualmente nuove Rules; Tell, don't ask, nessuna classe ha statement "return", quindi nessun oggetto chiede ad altri oggetti qualcosa, ma piuttosto comanda ai suoi collaboratori di eseguire qualcosa, ignorando i dettagli implementativi; infine l'architettura esagonale, ossia fare in modo che il dominio non abbia alcuna conoscenza del mondo esterno: difatti le classi di dominio non operano nessuna system.out, nè ritornano valori, piuttosto viene usata una generica interfaccia OutputPrinter, oltretutto non implementata se non nei test, e che potrebbe essere sostituita con una classe qualsiasi in grado di stampare su console, su web, su file.
Ovviamente sono anche rispettati i classici pattern come rimozione duplicazione e semplicità logica (pochi IF o cicli).
Sono graditi commenti :)
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.