Creare codice veramente modulare senza dipendenze

Sviluppare software è fantastico, ma think Penso che possiamo essere tutti d’accordo che può essere un po ‘ un ottovolante emotivo. All’inizio, tutto è grande. Si aggiungono nuove funzionalità una dopo l’altra in una questione di giorni se non ore. Sei su un rullo!

Avanti veloce di alcuni mesi e la velocità di sviluppo diminuisce. È perché non stai lavorando duramente come prima? Non proprio. Andiamo avanti velocemente un paio di mesi, e la velocità di sviluppo scende ulteriormente. Lavorare su questo progetto non è più divertente ed è diventato un peso.

Peggiora. Inizi a scoprire più bug nella tua applicazione. Spesso, risolvendo un bug ne crea due nuovi. A questo punto, puoi iniziare a cantare:

99 piccoli bug nel codice.99 piccoli insetti.Prendine uno, patchalo,

little 127 piccoli bug nel codice.

Come ti senti di lavorare a questo progetto ora? Se sei come me, probabilmente inizi a perdere la tua motivazione. È solo un dolore sviluppare questa applicazione, dal momento che ogni modifica al codice esistente può avere conseguenze imprevedibili.

Questa esperienza è comune nel mondo del software e può spiegare perché così tanti programmatori vogliono buttare via il loro codice sorgente e riscrivere tutto.

Motivi per cui lo sviluppo del software rallenta nel tempo

Quindi qual è la ragione di questo problema?

La causa principale è l’aumento della complessità. Dalla mia esperienza il più grande contributo alla complessità complessiva è il fatto che, nella stragrande maggioranza dei progetti software, tutto è connesso. A causa delle dipendenze che ogni classe ha, se si modifica un codice nella classe che invia e-mail, gli utenti improvvisamente non possono registrarsi. Perche ‘ mai? Perché il codice di registrazione dipende dal codice che invia e-mail. Ora non puoi cambiare nulla senza introdurre bug. Semplicemente non è possibile tracciare tutte le dipendenze.

Quindi ce l’hai; la vera causa dei nostri problemi sta aumentando la complessità proveniente da tutte le dipendenze che il nostro codice ha.

Grande palla di fango e come ridurlo

La cosa divertente è che questo problema è noto da anni. È un anti-modello comune chiamato ” grande palla di fango.”Ho visto questo tipo di architettura in quasi tutti i progetti a cui ho lavorato nel corso degli anni in più aziende diverse.

Quindi qual è esattamente questo anti-pattern? Semplicemente parlando, si ottiene una grande palla di fango quando ogni elemento ha una dipendenza con altri elementi. Di seguito, puoi vedere un grafico delle dipendenze dal noto progetto open-source Apache Hadoop. Per visualizzare la grande palla di fango (o meglio, la grande palla di filo), si disegna un cerchio e si posizionano le classi del progetto in modo uniforme su di esso. Basta tracciare una linea tra ogni coppia di classi che dipendono l’una dall’altra. Ora puoi vedere la fonte dei tuoi problemi.

Una visualizzazione della grande palla di fango di Apache Hadoop, con poche decine di nodi e centinaia di linee che li collegano tra loro.

“big ball of mud”di Apache Hadoop

Una soluzione con codice modulare

Quindi mi sono posto una domanda: Sarebbe possibile ridurre la complessità e divertirmi ancora come all’inizio del progetto? A dire il vero, non è possibile eliminare tutta la complessità. Se si desidera aggiungere nuove funzionalità, sarà sempre necessario aumentare la complessità del codice. Tuttavia, la complessità può essere spostata e separata.

Come altre industrie stanno risolvendo questo problema

Pensa all’industria meccanica. Quando qualche piccola officina meccanica sta creando macchine, acquistano un set di elementi standard, ne creano alcuni personalizzati e li mettono insieme. Possono creare quei componenti completamente separatamente e assemblare tutto alla fine, apportando solo alcune modifiche. Com’è possibile? Sanno come ogni elemento si adatta insieme da set standard di settore come bulloni dimensioni, e decisioni up-front come la dimensione dei fori di montaggio e la distanza tra di loro.

Un diagramma tecnico di un meccanismo fisico e come i suoi pezzi si incastrano. I pezzi sono numerati in ordine di cui allegare successivo, ma quell'ordine da sinistra a destra va 5, 3, 4, 1, 2.

Ogni elemento dell’assemblaggio di cui sopra può essere fornito da un’azienda separata che non ha alcuna conoscenza del prodotto finale o dei suoi altri pezzi. Fino a quando ogni elemento modulare è prodotto secondo le specifiche, si sarà in grado di creare il dispositivo finale come previsto.

Possiamo replicarlo nel settore del software?

Certo che possiamo! Utilizzando interfacce e inversione del principio di controllo; la parte migliore è il fatto che questo approccio può essere utilizzato in qualsiasi linguaggio orientato agli oggetti: Java, C#, Swift, TypeScript, JavaScript, PHP—l’elenco potrebbe continuare all’infinito. Non hai bisogno di alcun framework di fantasia per applicare questo metodo. Hai solo bisogno di attenersi ad alcune semplici regole e rimanere disciplinato.

Inversione di controllo è tuo amico

Quando ho sentito parlare di inversione di controllo, ho subito capito che avevo trovato una soluzione. È un concetto di prendere dipendenze esistenti e invertirle usando le interfacce. Le interfacce sono semplici dichiarazioni di metodi. Non forniscono alcuna implementazione concreta. Di conseguenza, possono essere utilizzati come accordo tra due elementi su come collegarli. Essi possono essere utilizzati come connettori modulari, se si vuole. Finché un elemento fornisce l’interfaccia e un altro elemento fornisce l’implementazione per esso, possono lavorare insieme senza sapere nulla l’uno dell’altro. E ‘ geniale.

Vediamo in un semplice esempio come possiamo disaccoppiare il nostro sistema per creare codice modulare. I diagrammi seguenti sono stati implementati come semplici applicazioni Java. Puoi trovarli su questo repository GitHub.

Problema

Supponiamo di avere un’applicazione molto semplice composta solo da una classe Main, tre servizi e una singola classe Util. Questi elementi dipendono l’uno dall’altro in più modi. Di seguito, puoi vedere un’implementazione utilizzando l’approccio “big ball of mud”. Le classi si chiamano semplicemente a vicenda. Sono strettamente accoppiati e non puoi semplicemente estrarre un elemento senza toccare gli altri. Le applicazioni create utilizzando questo stile consentono di crescere inizialmente rapidamente. Credo che questo stile sia appropriato per i progetti proof-of-concept poiché puoi giocare facilmente con le cose. Tuttavia, non è appropriato per soluzioni pronte per la produzione perché anche la manutenzione può essere pericolosa e qualsiasi singola modifica può creare bug imprevedibili. Lo schema seguente mostra questa grande palla di architettura fango.

Main utilizza i servizi A, B e C, che utilizzano ciascuno Util. Il Servizio C utilizza anche il Servizio A.

Perché Dependency Injection ha sbagliato tutto

In una ricerca di un approccio migliore, possiamo usare una tecnica chiamata dependency injection. Questo metodo presuppone che tutti i componenti debbano essere utilizzati tramite interfacce. Ho letto affermazioni che disaccoppia gli elementi, ma lo fa davvero, però? No. Dai un’occhiata allo schema qui sotto.

L'architettura precedente ma con iniezione di dipendenza. Ora Main utilizza il servizio di interfaccia A, B e C, che sono implementati dai loro servizi corrispondenti. I servizi A e C utilizzano entrambi il servizio di interfaccia B e l'interfaccia Util, implementata da Util. Il servizio C utilizza anche il servizio di interfaccia A. Ogni servizio insieme alla sua interfaccia è considerato un elemento.

L’unica differenza tra la situazione attuale e una grande palla di fango è il fatto che ora, invece di chiamare direttamente le classi, le chiamiamo attraverso le loro interfacce. Migliora leggermente la separazione degli elementi l’uno dall’altro. Se, ad esempio, si desidera riutilizzare Service A in un progetto diverso, è possibile farlo eliminando Service A stesso, insieme a Interface A, così come Interface B e Interface Util. Come puoi vedere, Service A dipende ancora da altri elementi. Di conseguenza, abbiamo ancora problemi con la modifica del codice in un posto e incasinando il comportamento in un altro. Crea ancora il problema che se si modificano Service B e Interface B, sarà necessario modificare tutti gli elementi che dipendono da esso. Questo approccio non risolve nulla; a mio parere, aggiunge solo uno strato di interfaccia sopra gli elementi. Non dovresti mai iniettare dipendenze, ma invece dovresti sbarazzartene una volta per tutte. Evviva l’indipendenza!

La soluzione per il codice modulare

L’approccio credo risolva tutti i principali mal di testa delle dipendenze non usando affatto le dipendenze. Si crea un componente e il relativo listener. Un ascoltatore è un’interfaccia semplice. Ogni volta che è necessario chiamare un metodo dall’esterno dell’elemento corrente, è sufficiente aggiungere un metodo al listener e chiamarlo invece. L’elemento è autorizzato solo a utilizzare file, chiamare metodi all’interno del suo pacchetto e utilizzare classi fornite dal framework principale o da altre librerie utilizzate. Di seguito, è possibile vedere un diagramma dell’applicazione modificata per utilizzare l’architettura degli elementi.

Un diagramma dell'applicazione modificato per utilizzare l'architettura degli elementi. Usi principali Util e tutti e tre i servizi. Main implementa anche un listener per ogni servizio, che viene utilizzato da quel servizio. Un ascoltatore e un servizio insieme sono considerati un elemento.

Si noti che, in questa architettura, solo la classe Main ha più dipendenze. Collega tutti gli elementi insieme e incapsula la logica di business dell’applicazione.

I servizi, invece, sono elementi completamente indipendenti. Ora, è possibile estrarre ogni servizio da questa applicazione e riutilizzarli da qualche altra parte. Non dipendono da nient’altro. Ma aspetta, c’è di meglio: non è necessario modificare tali servizi mai più, a patto che non si cambia il loro comportamento. Finché questi servizi fanno quello che dovrebbero fare, possono essere lasciati intatti fino alla fine dei tempi. Possono essere creati da un ingegnere software professionista o da un programmatore per la prima volta compromesso dal peggior codice di spaghetti che qualcuno abbia mai cucinato con dichiarazioni goto mescolate. Non importa, perché la loro logica è incapsulata. Per quanto orribile possa essere, non si riverserà mai su altre classi. Questo ti dà anche il potere di dividere il lavoro in un progetto tra più sviluppatori, in cui ogni sviluppatore può lavorare sul proprio componente in modo indipendente senza la necessità di interromperne un altro o addirittura conoscere l’esistenza di altri sviluppatori.

Infine, puoi iniziare a scrivere codice indipendente ancora una volta, proprio come all’inizio del tuo ultimo progetto.

Pattern Elemento

Definiamo il pattern elemento strutturale in modo che saremo in grado di crearlo in modo ripetibile.

La versione più semplice dell’elemento consiste di due cose: Una classe elemento principale e un ascoltatore. Se si desidera utilizzare un elemento, è necessario implementare il listener ed effettuare chiamate alla classe principale. Ecco un diagramma della configurazione più semplice:

Un diagramma di un singolo elemento e il suo ascoltatore all'interno di un'app. Come prima, l'app utilizza l'elemento, che utilizza il suo listener, che è implementato dall'app.

Ovviamente, dovrai aggiungere più complessità all’elemento alla fine, ma puoi farlo facilmente. Assicurati solo che nessuna delle tue classi logiche dipenda da altri file nel progetto. Possono utilizzare solo il framework principale, le librerie importate e altri file in questo elemento. Quando si tratta di file di risorse come immagini,visualizzazioni, suoni, ecc., loro anche dovrebbero essere incapsulati all’interno di elementi così che nel futuro loro saranno facili da riutilizzare. Si può semplicemente copiare l’intera cartella in un altro progetto ed eccolo!

Di seguito, puoi vedere un grafico di esempio che mostra un elemento più avanzato. Si noti che è costituito da una vista che sta utilizzando e non dipende da altri file di applicazione. Se vuoi conoscere un metodo semplice per controllare le dipendenze, guarda la sezione importa. Ci sono file dall’esterno dell’elemento corrente? In tal caso, è necessario rimuovere tali dipendenze spostandole nell’elemento o aggiungendo una chiamata appropriata al listener.

Un semplice diagramma di un elemento più complesso. Qui, il senso più ampio della parola "elemento" consiste di sei parti: Vista; Logiche A, B e C; Elemento; e Listener di elementi. Le relazioni tra questi ultimi due e l

Diamo anche un’occhiata a un semplice esempio di “Hello World” creato in Java.

public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String args) { App app = new App(); app.start(); }}

Inizialmente, definiamo ElementListener per specificare il metodo che stampa l’output. L’elemento stesso è definito di seguito. Chiamando sayHellosull’elemento, stampa semplicemente un messaggio usando ElementListener. Si noti che l’elemento è completamente indipendente dall’implementazione del metodo printOutput. Può essere stampato nella console, una stampante fisica o un’interfaccia utente di fantasia. L’elemento non dipende da tale implementazione. A causa di questa astrazione, questo elemento può essere riutilizzato in diverse applicazioni facilmente.

Ora dai un’occhiata alla classe principale App. Implementa l’ascoltatore e assembla l’elemento insieme all’implementazione concreta. Ora possiamo iniziare ad usarlo.

Puoi anche eseguire questo esempio in JavaScript qui

Element Architecture

Diamo un’occhiata all’utilizzo del pattern element in applicazioni su larga scala. Una cosa è mostrarlo in un piccolo progetto—è un altro applicarlo al mondo reale.

La struttura di un’applicazione Web full-stack che mi piace usare appare come segue:

src├── client│ ├── app│ └── elements│ └── server ├── app └── elements

In una cartella del codice sorgente, inizialmente abbiamo diviso i file client e server. È una cosa ragionevole da fare, dal momento che funzionano in due ambienti diversi: il browser e il server back-end.

Quindi dividiamo il codice in ogni livello in cartelle chiamate app ed elementi. Elementi è costituito da cartelle con componenti indipendenti, mentre i fili cartella app tutti gli elementi insieme e memorizza tutta la logica di business.

In questo modo, gli elementi possono essere riutilizzati tra diversi progetti, mentre tutta la complessità specifica dell’applicazione viene incapsulata in una singola cartella e spesso ridotta a semplici chiamate agli elementi.

Esempio pratico

Credendo che la pratica briscola sempre la teoria, diamo un’occhiata a un esempio di vita reale creato in Node.js e dattiloscritto.

Esempio di vita reale

È un’applicazione web molto semplice che può essere utilizzata come punto di partenza per soluzioni più avanzate. Segue l’architettura dell’elemento e utilizza un modello di elemento ampiamente strutturale.

Dagli highlights, puoi vedere che la pagina principale è stata distinta come elemento. Questa pagina include la propria vista. Quindi, quando, ad esempio, vuoi riutilizzarlo, puoi semplicemente copiare l’intera cartella e rilasciarla in un progetto diverso. Basta collegare tutto insieme e si è a posto.

È un esempio di base che dimostra che è possibile iniziare a introdurre elementi nella propria applicazione oggi. Puoi iniziare a distinguere i componenti indipendenti e separare la loro logica. Non importa quanto sia disordinato il codice su cui stai lavorando.

Sviluppare più velocemente, riutilizzare più spesso!

Spero che, con questo nuovo set di strumenti, sarai in grado di sviluppare più facilmente codice che sia più manutenibile. Prima di saltare in utilizzando il modello di elemento in pratica, ricapitoliamo rapidamente tutti i punti principali:

  • Molti problemi nel software si verificano a causa delle dipendenze tra più componenti.

  • Facendo un cambiamento in un posto, puoi introdurre comportamenti imprevedibili da qualche altra parte.

Tre approcci architettonici comuni sono:

  • La grande palla di fango. È ottimo per un rapido sviluppo, ma non così grande per scopi di produzione stabili.

  • Iniezione di dipendenza. È una soluzione mezza cotta che dovresti evitare.

  • Architettura degli elementi. Questa soluzione consente di creare componenti indipendenti e riutilizzarli in altri progetti. È mantenibile e brillante per rilasci di produzione stabili.

Il modello di elemento di base è costituito da una classe principale che ha tutti i principali metodi e un ascoltatore che è una semplice interfaccia che consente la comunicazione con il mondo esterno.

Per ottenere un’architettura di elementi full-stack, prima si separa il front-end dal codice back-end. Quindi si crea una cartella in ciascuna per un’app e elementi. La cartella elements è composta da tutti gli elementi indipendenti, mentre la cartella app collega tutto insieme.

Ora puoi iniziare a creare e condividere i tuoi elementi. A lungo termine, ti aiuterà a creare prodotti facilmente manutenibili. Buona fortuna e fammi sapere cosa hai creato!

Inoltre, se ti trovi a ottimizzare prematuramente il tuo codice, leggi Come evitare la maledizione dell’ottimizzazione prematura del collega Toptaler Kevin Bloch.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.