Architettura pulita: come riflettere le modifiche del livello dati nell’interfaccia utente

Sto cercando di creare un progetto basato su Clean Architecture di Uncle Bob in Android.

Il problema:

Mi piacerebbe risolvere è come fare in modo che le modifiche generate in un repository si riflettano in altre parti dell’app, come altri repository o Views.

L’esempio

Ho progettato un esempio MOLTO semplificato per questo esempio. Si prega di notare che le interfacce di confine sono state rimosse per mantenere i diagrammi piccoli.

Immagina un’app che mostri un elenco di video (con titolo, cifra e conteggi simili), facendo clic su un video puoi vedere il dettaglio (lì puoi / non mi piace il video).

Inoltre l’app ha un sistema di statistiche che conta il numero di video che l’utente ha gradito o meno.

Le classi principali per questa app potrebbero essere:

Per la parte / modulo Video: inserisci la descrizione dell'immagine qui

Per la parte / modulo Stats: inserisci la descrizione dell'immagine qui

Il bersaglio

Ora immagina di controllare le tue statistiche, quindi navigare nell’elenco dei video, aprire i dettagli di uno e fare clic sul pulsante Mi piace.

L’after-like viene inviato al server, ci sono diversi elementi delle app che dovrebbero essere consapevoli della modifica:

  • Ovviamente la vista di dettaglio, dovrebbe essere aggiornata con le modifiche (questo può essere fatto attraverso callback quindi nessun problema)
  • L’elenco di video dovrebbe aggiornare il conteggio “Mi piace” per il video specificato
  • Il `StatsRepository ‘potrebbe voler aggiornare / invalidare le cache dopo aver votato un nuovo video
  • Se l’elenco delle statistiche è visibile (immagina uno schermo diviso) dovrebbe anche mostrare le statistiche aggiornate (o almeno ricevere l’evento per ri-interrogare i dati)

La domanda

Quali sono i modelli comuni per risolvere questo tipo di comunicazione? Si prega di fornire una risposta il più completa ansible, specificando dove vengono generati gli eventi, come vengono propagati attraverso l’app, ecc.

Nota: verranno dati i Taglie per completare le risposte

    Pubblica / sottoscrivi

    In genere, per la comunicazione n: m (n mittenti possono inviare un messaggio ai m ricevitori, mentre tutti i mittenti e destinatari non si conoscono) si utilizzerà un modello di pubblicazione / sottoscrizione . Ci sono molte librerie che implementano questo tipo di comunicazione, per Java esiste ad esempio un’implementazione EventBus nella libreria Guava . Per le comunicazioni in-app queste librerie sono in genere denominate EventBus o EventManager e inviano / ricevono eventi .

    Eventi di dominio

    Supponiamo che tu abbia ora creato un evento VideoRatedEvent , che segnala che a un utente è piaciuto o che non è piaciuto un video. Questi tipi di eventi sono indicati come eventi di dominio . La class evento è un POJO semplice e potrebbe avere il seguente aspetto:

     class VideoRatedEvent { /** The video that was rated */ public Video video; /** The user that triggered this event */ public User user; /** True if the user liked the video, false if the user disliked the video */ public boolean liked; } 

    Eventi di spedizione

    Ora ogni volta che ai tuoi utenti piace o non piace un video, dovrai inviare un VideoRatedEvent . Con Guava, dovrai semplicemente passare un object evento istanziato per opporsi a EventBus.post(myVideoRatedEvent) . Idealmente gli eventi sono generati negli oggetti del tuo dominio e vengono inviati all’interno della transazione persistente (vedi questo post del blog per i dettagli). Ciò significa che man mano che lo stato del modello di dominio viene mantenuto, gli eventi vengono inviati.

    Ascoltatori di eventi

    Nell’applicazione, tutti i componenti interessati da un evento possono ora ascoltare gli eventi del dominio. Nel tuo particolare esempio, VideoDetailView o StatsRepository potrebbero essere listener di eventi per VideoRatedEvent . Naturalmente, è necessario registrarli al EventBus Guava con EventBus.register(Object) .

    Questi sono i miei 5 punti personali e forse non strettamente correlati al tuo esempio di “The Clean Architecure”.

    Di solito cerco di forzare un tipo di MVC su attività e frammenti di androidi e di utilizzare publish / subscribe per la comunicazione. Come componenti ho classi di modelli che gestiscono la logica aziendale e lo stato dei dati. I metodi di modifica dei dati devono essere chiamati solo dalle classi controller che di solito è la class di attività e gestisce anche lo stato della sessione. Uso i frammenti per gestire diverse parti di vista dell’applicazione e le viste sotto questi frammenti (ovviamente). Tutti i frammenti si iscrivono a uno o più argomenti. Uso il mio semplice DataDistributionService che gestisce diversi argomenti, prende i messaggi dagli editori registrati e li inoltra a tutti gli abbonati. (in parte influenzato dal DDS OMG ma MOLTO MOLTO più primitivo) Una semplice applicazione avrebbe solo un singolo argomento, ad esempio “Principale”.

    Ogni parte dell’interazione della vista (tocchi, ecc.) Viene gestita prima dal suo frammento. Il frammento può potenzialmente modificare alcune cose senza inviare notifiche. Ad esempio, cambiare il subrange degli elementi di dati resi se il resto dell’app non ha bisogno di sapere / reactjs. Altrimenti il ​​frammento pubblica una ViewRequest (…) contenente i parametri necessari al DDS.

    Il DDS trasmette quel messaggio e ad un certo punto raggiunge un controller. Questo può essere semplicemente l’attività principale o un’istanza specifica del controller. Ci dovrebbe essere un solo controller in modo che la richiesta venga gestita solo una volta. Il controller ha fondamentalmente una lunga lista di codice per la gestione delle richieste. Quando arriva una richiesta, il controller chiama la business logic nel modello. Il controller gestisce anche altre cose relative alla vista come organizzare la vista (tabs) o avviare le windows di dialogo per l’input dell’utente (sovrascrivere il file?) E altre cose che il modello non dovrebbe conoscere ma influenze (Lanciare una nuova NoOverWritePermissionException ())

    Una volta apportate le modifiche al modello, il controllore decide se inviare una notifica di aggiornamento. (di solito lo fa). In questo modo le classi modello non hanno bisogno di ascoltare o inviare messaggi e si occupano solo della logica di busines e dello stato coerente. La notifica di aggiornamento è trasmessa e ricevuta dai frammenti che poi eseguono “updateFromModel ()”.

    effetti:
    I comandi sono globali. Qualsiasi richiesta di richiesta o altro tipo di richiesta può essere inviata da qualsiasi luogo in cui è ansible accedere al DDS. I frammenti non devono fornire una class listener e nessuna istanza più alta deve implementare i listener per i loro frammenti istanziati. Se un nuovo frammento non richiede nuove Richieste può essere aggiunto senza modifiche alle classi del controllore.

    Le classi modello non devono assolutamente conoscere la comunicazione. Può essere abbastanza difficile mantenere uno stato coerente e gestire tutta la gestione dei dati. Non sono necessarie la gestione dei messaggi o la gestione dello stato della sessione. Tuttavia, il modello potrebbe non essere protetto da chiamate maligne dalla vista. Ma questo è un problema generale e non può essere davvero impedito se il modello deve dare dei riferimenti ad un certo punto. Se la tua app funziona bene con un modello che trasmette solo copie / dati flat è ansible. Ma ad un certo punto l’ArrayAdapter ha semplicemente bisogno di accedere alle bitmap che dovrebbe disegnare nella griglia. Se non ti puoi permettere le copie, hai sempre il rischio che “la vista faccia cambiare la chiamata al modello”. Campo di battaglia diverso …

    Le chiamate di aggiornamento potrebbero essere troppo semplici. Se l’aggiornamento di un frammento è costoso (frammento di OpenGL che ricarica le trame …) si desidera avere informazioni di aggiornamento più dettagliate. Il controllore POTREBBE inviare una notifica più dettagliata, ma in realtà non dovrebbe avere / essere in grado di sapere quali parti del modello sono state cambiate esattamente. L’invio di note di aggiornamento dal modello è brutto. Non solo il modello deve implementare la messaggistica, ma diventa anche molto caotico con notifiche miste. Il controllore può dividere un po ‘le notifiche di aggiornamento e altre usando argomenti. Ad esempio un argomento specifico per le modifiche alle risorse video. In questo modo i frammenti possono decidere quali argomenti sottoscrivere. Oltre a questo si desidera avere un modello che può essere interrogato per i valori modificati. Timestamp, ecc. Ho un’app in cui l’utente disegna forms su canvas. Vengono renderizzati in bitmap e utilizzati come trame in una vista OpenGL. Certamente non voglio ricaricare textures ogni volta che “updateFromModel ()” viene chiamato in GLViewFragment.

    Regola di dipendenza:
    Probabilmente non è rispettato tutto il tempo. Se il controller gestisce un interruttore di tabulazione può semplicemente chiamare “seletTab ()” su un TabHost e quindi avere una dipendenza dai cerchi esterni. Puoi trasformarlo in un messaggio, ma è ancora una dipendenza logica. Se la parte del controller deve organizzare alcuni elementi della vista (mostrare la scheda image-editor-frammentazione automaticamente dopo aver caricato un’immagine tramite la scheda image-gallery-fragmen-tab) non è ansible evitare completamente le dipendenze. Forse puoi farlo eseguendo la modellazione di viewstate e organizzare le parti di visualizzazione da viewstate.currentUseCase o smth in questo modo. Ma se hai bisogno di un controllo globale sulla vista della tua app, avrai dei problemi con questa regola di dipendenza, direi. Cosa succede se provi a salvare alcuni dati e il tuo modello richiede l’authorization per la sovrascrittura? È necessario creare una sorta di interfaccia utente per questo. Dipendenza di nuovo. È ansible inviare un messaggio alla vista e sperare che un DialogFragment lo prelevi. Se esiste nel mondo estremamente modulare descritto al tuo link.

    Entità:
    sono le classi modello nel mio approccio. Questo è abbastanza vicino al link che hai fornito.

    Casi d’uso:
    Non ho quelli esplicitamente modellati per ora. Atm Sto lavorando agli editori per i beni dei videogiochi. Disegnare forms in un frammento, applicare i valori di ombreggiatura in un altro frammento, salvare / caricare in un frammento di gallerie, esportare in un atlante di texture in un altro … cose del genere. Aggiungerei Use Case come una sorta di sottoinsieme di richieste. Fondamentalmente un caso d’uso come un insieme di regole che richiedono in quale ordine sono permessi / richiesti / previsti / vietati ecc. Vorrei costruirli come le transazioni in modo che un caso d’uso possa continuare a progredire, possa essere finito, possa essere cancellato e magari anche laminato indietro. Ad esempio, un caso d’uso definirà l’ordine di salvataggio di un’immagine fresca disegnata. Includere la pubblicazione di una finestra di dialogo per chiedere l’authorization per la sovrascrittura e il rollback se l’authorization non viene concessa o se viene raggiunto il timeout. Ma gli Use Case sono definiti in molti modi diversi. Alcune app hanno un singolo caso d’uso per un’ora di interazione con l’utente attivo, alcune app hanno 50 casi d’uso solo per ottenere denaro da un bancomat. 😉

    Adattatori di interfaccia:
    Qui diventa un po ‘complicato. Per me questo sembra essere un livello estremamente alto per le app Android. Si afferma “The Ring of Interface Adapter contiene l’intera architettura MVC di una GUI”. Non riesco davvero a capirlo. Forse stai costruendo applicazioni molto più complicate di me.

    Quadri e driver:
    Non sono sicuro di cosa pensare di questo. “Il web è un dettaglio, il database è un dettaglio …” e il grafico contiene “UI” anche in questo Ring. Troppo per la mia piccola testa

    Consente di controllare gli altri “asserzioni”
    Indipendente dai Framework. L’architettura non dipende dall’esistenza di alcune librerie di software a funzionalità elevata. Ciò ti consente di utilizzare tali framework come strumenti, piuttosto che dover stipare il tuo sistema nei loro limiti limitati.
    Hmm si, bene, se gestisci la tua architettura è quello che ottieni.

    Verificabile. Le regole aziendali possono essere testate senza interfaccia utente, database, server Web o qualsiasi altro elemento esterno.
    Come nel mio approccio, le classi di modelli non conoscono né i controllori né le viste né il passaggio del messaggio. Si può verificare la coerenza dello stato solo con quelle sole classi.

    Indipendente dall’interfaccia utente. L’interfaccia utente può cambiare facilmente, senza modificare il resto del sistema. Un’interfaccia utente Web potrebbe essere sostituita con un’interfaccia utente della console, ad esempio, senza modificare le regole aziendali.
    Ancora una volta un po ‘eccessivo per Android non è vero? Indipendenza si. Nel mio approccio è ansible aggiungere o rimuovere i frammenti purché non richiedano una gestione esplicita da qualche parte più in alto. Ma sostituire un’interfaccia utente Web con un’interfaccia utente di console e far funzionare il sistema come prima è un sogno bagnato di appassionati dell’architettura. Alcuni elementi dell’interfaccia utente sono parte integrante del servizio fornito. Ovviamente posso facilmente scambiare il frammento di disegno su canvas per un frammento di disegno di una console, o il classico frammento di foto per un frammento di ‘scattare foto con console’ ma questo non significa che l’applicazione funzioni ancora. Tecnicamente sta bene nel mio approccio. Se implementi un player video della console ascii, puoi renderizzare i video lì e nessuna altra parte dell’app dovrà necessariamente interessare. Tuttavia, potrebbe essere che l’insieme di richieste che il controller supporta non si allinea bene con la nuova interfaccia utente della console o che un caso d’uso non sia progettato per l’ordine in cui è necessario accedere a un video tramite un’interfaccia console. Il punto di vista non è sempre lo schiavo di presentazione senza importanza che molti guru dell’architettura amano vederlo.

    Indipendente dal database. È ansible scambiare Oracle o SQL Server, per Mongo, BigTable, CouchDB o altro. Le tue regole aziendali non sono legate al database.
    Si, quindi? Com’è collegato direttamente alla tua architettura? Usa gli adattatori e l’astrazione giusti e puoi averli in un’app ciao mondo.

    Indipendente da qualsiasi agenzia esterna. In realtà le tue regole aziendali semplicemente non sanno nulla del mondo esterno.
    Anch’io. Se vuoi un codice indipendente modulare, scriverlo. Difficile dire qualcosa di specifico a riguardo.