Dal mio punto di vista il punto principale nel realizzare un software è riuscire ad applicare i design pattern. Perchè? Perchè i design pattern sono il risultato dell’esperienza di chi ci è già passato. Un po’ come un genitore, che consiglia il proprio figlio sui pro e contro delle scelte della sua vita… Lui sa già come andrà a finire! Ad esempio il Command pattern, Mediator, e altri, sono alla base di Redux.
Qual’è il più grosso problema di un software pensando al lungo termine? Per me sono la manutenibilità e la flessibilità ad “accettare” (implementare) nuovi requisiti. In rarissimi casi – direi molto speciali – ci sono le performance. Ma per me, se ci sono problemi di performance, non è di certo una “scorciatoia” che risolverà il problema, ma significa che ci sono errori di architettura del software. Tipico esempio sono i monoliti, che se trasformati in CQRS (con un message bus) diventano delle Ferrari. Morale: non spendere tempo ad aumentare le performance di un monolite, impiega lo stesso tempo invece a migliorare l’architettura.
Nel caso di una tipica applicazione Angular abbiamo lo stesso scenario. Quindi secondo me, applicando i principi SOLID, e in particolare il “Single responsibility”, facciamo una scelta a lungo termine. “Singola responsabilità” significa che una classe (o una porzione isolata di codice) ha una e una sola responsabilità. Questo concetto può essere applicato a diverse “altezze”, sia nel codice di una classe, sia a livello di architettura.
Quindi, vedendo questo applicato a Angular con Redux e json REST API, direi che:
- Angular template: ha la sola responsabilità di visualizzare i dati, quindi nessuna business logic in html, i dati vengono presi dal component, e vengono trasformati solo tramite pipe. In pratica: nessun javascript che manipola i dati (e poi riflettiamo… come scrivi il unit-test per quella “funzionalità”?).
- Angular component: ha la responsabilità di comunicare i comandi/azioni al backend e fornire i dati da visualizzare al template. Anche qui, nessuna business logic, e se proprio dobbiamo, solo minime trasformazioni di dati per il template.
- Selectors: ha la responsabilità di trasformare i dati residenti nello store e combinarli tra diversi store per essere fruibili dal component.
- Store + reducers: ha la singola responsabilità di trasformare le azioni e relativo payload in uno “stato dell’applicazione” grazie ai reducers.
- Effect: ha la responsabilità di trasformare una azione in una chiamata al backend (tramite il backend service), ed emettere la response con una nuova azione (che io chiamerei invece “evento”). Non contiene business logic. In realtà l’effect viene chiamato dal dispatcher degli eventi (e non dallo store), ma ho preferito disegnarlo così per semplicità. In origine era chiamato “middleware”.
- Backend service: ha la responsabilità di comunicare con il le REST API del backend, e anche qui non contiene business logic.
- Backend REST API: ha la responsabilità nello storage dei dati (non entro nel dettaglio, potete vedere un altro mio articolo).
Ogni parte descritta è un layer a se stante, e quindi può solo comunicare con il layer direttamente collegato (e preferibilmente in modo unidirezionale). Se notate dallo schema, non ci sono frecce bi-direzionali, ma il flusso dati è predicibile e ben chiaro.
Alcune note sulle singole parti:
- Backend REST API: i metodi sono esposti tramite route, utilizzando correttamente i metodi GET, POST, PUT, DELETE ed evitando i “body” con mille parametri che fanno-tutto. Anche qui trovate molta documentazione a riguardo.
Ad esempio GET /products/{productId}/ è meglio di GET /products/GetProductsById - Backend service: fa solo da pass-through, e deve essere accessibile solo dagli effect
- Effect: Reagisce solo ad alcune action command (es. GetProductsAction) che richiedono il backend (in questo caso lo Store non fa nulla) e dopo aver ricevuto i dati dal backend service, emette una nuova action con payload “GetProductsReceivedEvent” o “GetProductsFailedEvent” (in caso di errore) che saranno processati poi dallo Store.
Nota: Preferisco questa denominazione (con il verbo al passato) presa dai sistemi di messaggistica (tipo RabbitMQ) che mi sembra più chiara e esplicita, invece di chiamare tutto e tutti “Action”. - Store: Riceve gli eventi e relativo payload, lo trasforma attuando le giusto proiezioni con i reducers e lo salva come stato. Qui abbiamo la business logic e il routing degli eventi.
E’ buona pratica dividere gli store in moduli/aree/concetti ed evitare un unico gigante store. Dipende molto dal tipo di applicazione, ma – a livello di concetto – è possibile avere anche più store per modulo; il punto pratico è che vogliamo notificare (e quindi far reagire, in quando lo store emetterebbe un nuovo valore) solo chi veramente ne ha bisogno. - Selectors: Espone i dati utilizzabili dal component, e applica le giuste proiezioni, se necessario utilizzando altri selectors per generare dati utili. Quindi anche questo layer contiene business logic, ma solo in come viene esposta, in pratica come le informazioni vengono “combinate” assieme. Anche qui è buona pratica dividere i selectors come gli store, in modo da esporre “concetti” piuttosto che un blob di informazioni eterogenee.
- Angular component: il component è in “ascolto” degli observable esposti dai selectors, e reagisce all’emissione di nuovi valori. Eventuali eventi vengono trasformati in azioni verso lo store. Nota: nello store non viene inviato tutto (ad esempio i contenuti delle form) ma solo gli eventi che corrispondono allo stato condiviso dell’applicazione. In pratica, se non interessa a nessun altro, lo stato rimane locale al component. Un caso pratico: la pressione del pulsante “Salva” di una form che richiede di storicizzare un nuovo prodotto, che sarà necessario in futuro.
- Angular template: la visualizzazione è passiva, reagisce solo ai nuovi valori emessi. Aggiorna i dati sottoscrivendosi agli eventi emessi dagli observables. In pratica un subscribe su un observable (esposto dal selector).
Per le Form: tramite BehaviorSubject o direttamente da eventi generati per noi dal FormControl, possono emettere eventi che possiamo combinare con altri stream per poi generare comandi. Ad esempio una casella di testo che abbinata a debounceTime e distinctUntilChanged emette una richiesta per una nuova ricerca.
Tu cosa ne pensi? Hai qualche consiglio o esperienza da condividere?
Spero di essere stato utile, scrivetemi pure!
Articolo ben fatto! mi ha chiarito molti dubbi sopratutto la parte della trasformazione dei dati che è un compito strettamente riservato alle pipe, aihme quante volte sbagliando ho trasformato il dato direttamente nel componente…ma scagli la prima pietra chi è senza peccato…:-)
Grazie mille ci vediamo al prossimo articolo.
Andrea
Ottimo, mi fa piacere ti sia stato utile! Grazie a te e al tempo che hai dedicato a leggerlo