AngularJS: comprensione del modello di progettazione

Nel contesto di questo post di Igor Minar, capo di AngularJS:

MVC vs MVVM vs MVP . Che argomento controverso che molti sviluppatori possono trascorrere ore e ore a discutere e discutere.

Per diversi anni AngularJS era più vicino a MVC (o piuttosto a una delle sue varianti client-side), ma nel tempo e grazie a numerosi refactoring e miglioramenti API, ora è più vicino a MVVM : l’object $ scope può essere considerato il ViewModel che viene decorato da una funzione che chiamiamo Controller .

Essere in grado di classificare un framework e inserirlo in uno dei bucket MV * presenta alcuni vantaggi. Può aiutare gli sviluppatori a sentirsi più a proprio agio con le proprie apis rendendo più facile la creazione di un modello mentale che rappresenti l’applicazione che viene costruita con il framework. Può anche aiutare a stabilire la terminologia utilizzata dagli sviluppatori.

Detto questo, preferirei che gli sviluppatori costruissero app kick-ass ben progettate e seguite la separazione delle preoccupazioni, piuttosto che vederle perdere tempo a discutere di sciocchezze di MV *. E per questo motivo, dichiaro con la presente che AngularJS è il framework MVW – Model-View-Whatever . Dove Tutto sta per ” qualunque cosa funzioni per te “.

Angular offre molta flessibilità per separare in modo elegante la logica di presentazione dalla logica aziendale e dallo stato della presentazione. Si prega di usarlo per alimentare la produttività e la manutenibilità delle applicazioni piuttosto che discussioni accese su cose che alla fine della giornata non contano più di tanto.

Esistono raccomandazioni o linee guida per l’implementazione del pattern di progettazione di AngularJS MVW (Model-View-Whatever) nelle applicazioni lato client?

Grazie a un’enorme quantità di risorse preziose ho ricevuto alcune raccomandazioni generali per l’implementazione dei componenti nelle app AngularJS:


controllore

  • Il controller dovrebbe essere solo un intercalare tra modello e vista. Cerca di renderlo il più sottile ansible.

  • Si consiglia vivamente di evitare la logica aziendale nel controller. Dovrebbe essere spostato sul modello.

  • Il controllore può comunicare con altri controllori usando la chiamata al metodo (ansible quando i bambini vogliono comunicare con i genitori) o i metodi $ emit , $ broadcast e $ on . I messaggi emessi e trasmessi dovrebbero essere ridotti al minimo.

  • Il controller non dovrebbe interessarsi alla presentazione o alla manipolazione del DOM.

  • Cerca di evitare i controller annidati . In questo caso il controllore genitore viene interpretato come modello. Inietta invece i modelli come servizi condivisi.

  • L’ambito del controller deve essere utilizzato per il modello di binding con view e
    Incapsulare il modello di vista come per il modello di progettazione del modello di presentazione.


Scopo

Tratta l’ambito come di sola lettura nei modelli e in sola scrittura nei controller . Lo scopo dell’ambito è fare riferimento al modello, non essere il modello.

Quando si esegue il bind bidirezionale (ng-model) assicurarsi di non eseguire il binding diretto alle proprietà dell’ambito.


Modello

Il modello in AngularJS è un singleton definito dal servizio .

Il modello fornisce un modo eccellente per separare i dati e la visualizzazione.

I modelli sono i candidati principali per il test delle unità, poiché in genere hanno esattamente una dipendenza (una forma di emettitore di eventi, nel caso comune il $ rootScope ) e contengono una logica di dominio altamente testabile.

  • Il modello dovrebbe essere considerato come un’implementazione di una particolare unità. Si basa sul principio della singola responsabilità. L’unità è un’istanza responsabile del proprio ambito di logica correlata che può rappresentare una singola quadro nel mondo reale e descriverla nel mondo della programmazione in termini di dati e stato .

  • Il modello dovrebbe incapsulare i dati dell’applicazione e fornire un’API per accedere e manipolare tali dati.

  • Il modello deve essere portatile in modo che possa essere facilmente trasportato a un’applicazione simile.

  • Isolando la logica dell’unità nel modello, è stato facilitato l’individuazione, l’aggiornamento e la manutenzione.

  • Il modello può utilizzare metodi di modelli globali più generali comuni per l’intera applicazione.

  • Cerca di evitare la composizione di altri modelli nel tuo modello usando l’iniezione di dipendenza, se non è realmente dipendente dalla diminuzione dell’accoppiamento dei componenti e dall’aumento della testabilità e dell’usabilità dell’unità.

  • Cerca di evitare l’uso di listener di eventi nei modelli. Rende loro più difficile testare e in genere uccide i modelli in termini di principio di responsabilità singola.

Implementazione del modello

Dato che il modello dovrebbe incapsulare una certa logica in termini di dati e stato, dovrebbe limitare in maniera architettonica l’accesso ai suoi membri, così possiamo garantire un accoppiamento lento.

Il modo per farlo nell’applicazione AngularJS è definirlo utilizzando il tipo di servizio di fabbrica . Questo ci permetterà di definire le proprietà e i metodi privati ​​in modo molto semplice e restituiremo anche quelli accessibili pubblicamente in un unico posto che lo renderà veramente leggibile per gli sviluppatori.

Un esempio :

angular.module('search') .factory( 'searchModel', ['searchResource', function (searchResource) { var itemsPerPage = 10, currentPage = 1, totalPages = 0, allLoaded = false, searchQuery; function init(params) { itemsPerPage = params.itemsPerPage || itemsPerPage; searchQuery = params.substring || searchQuery; } function findItems(page, queryParams) { searchQuery = queryParams.substring || searchQuery; return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) { totalPages = results.totalPages; currentPage = results.currentPage; allLoaded = totalPages <= currentPage; return results.list }); } function findNext() { return findItems(currentPage + 1); } function isAllLoaded() { return allLoaded; } // return public model API return { /** * @param {Object} params */ init: init, /** * @param {Number} page * @param {Object} queryParams * @return {Object} promise */ find: findItems, /** * @return {Boolean} */ allLoaded: isAllLoaded, /** * @return {Object} promise */ findNext: findNext }; }); 

Creare nuove istanze

Cerca di evitare di avere una fabbrica che restituisca una nuova funzione in quanto inizia a scomporre l'iniezione della dipendenza e la libreria si comporterà in modo imbarazzante, soprattutto per le terze parti.

Un modo migliore per realizzare la stessa cosa è usare la fabbrica come API per restituire una collezione di oggetti con metodi getter e setter ad essi collegati.

 angular.module('car') .factory( 'carModel', ['carResource', function (carResource) { function Car(data) { angular.extend(this, data); } Car.prototype = { save: function () { // TODO: strip irrelevant fields var carData = //... return carResource.save(carData); } }; function getCarById ( id ) { return carResource.getById(id).then(function (data) { return new Car(data); }); } // the public API return { // ... findById: getCarById // ... }; }); 

Modello globale

In generale, cerca di evitare tali situazioni e progetta i tuoi modelli correttamente in modo che possa essere iniettato nel controller e utilizzato nella visualizzazione.

In particolare, alcuni metodi richiedono l'accessibilità globale all'interno dell'applicazione. Per renderlo ansible è ansible definire la proprietà ' comune ' in $ rootScope e collegarlo a commonModel durante il bootstrap dell'applicazione:

 angular.module('app', ['app.common']) .config(...) .run(['$rootScope', 'commonModel', function ($rootScope, commonModel) { $rootScope.common = 'commonModel'; }]); 

Tutti i tuoi metodi globali vivranno all'interno di una proprietà ' comune '. Questa è una specie di spazio dei nomi .

Ma non definire alcun metodo direttamente nel tuo $ rootScope . Ciò può comportare un comportamento imprevisto quando viene utilizzato con la direttiva ngModel all'interno dell'ambito di visualizzazione, in genere sporcando l'ambito e conducendo ai metodi di ambito che annullano i problemi.


Risorsa

La risorsa ti consente di interagire con diverse fonti di dati .

Dovrebbe essere implementato usando il principio della singola responsabilità .

In particolare, è un proxy riutilizzabile per gli endpoint HTTP / JSON.

Le risorse sono iniettate nei modelli e offrono la possibilità di inviare / recuperare i dati.

Implementazione delle risorse

Un factory che crea un object risorsa che consente di interagire con le origini dati RESTful sul lato server.

L'object risorsa restituito ha metodi di azione che forniscono comportamenti di alto livello senza la necessità di interagire con il servizio $ http di basso livello.


Servizi

Sia il modello che la risorsa sono servizi .

I servizi sono unità di funzionalità non associate e liberamente accoppiate che sono autonome.

I servizi sono una funzionalità che Angular porta alle app Web lato client dal lato server, dove i servizi sono stati comunemente utilizzati per un lungo periodo di tempo.

I servizi nelle app angolari sono oggetti intercambiabili collegati insieme mediante l'integrazione delle dipendenze.

Angolare viene fornito con diversi tipi di servizi. Ognuno con i propri casi d'uso. Si prega di leggere Informazioni sui tipi di servizi per i dettagli.

Prova a considerare i principi fondamentali dell'architettura dei servizi nella tua applicazione.

In generale, in base al Glossario dei servizi Web :

Un servizio è una risorsa astratta che rappresenta una capacità di eseguire attività che formano una funzionalità coerente dal punto di vista delle quadro dei provider e delle quadro dei richiedenti. Per essere utilizzato, un servizio deve essere realizzato da un agente fornitore concreto.


Struttura lato client

In generale, il lato client dell'applicazione è suddiviso in moduli . Ogni modulo dovrebbe essere testabile come unità.

Prova a definire i moduli in base a funzionalità / funzionalità o vista , non in base al tipo. Vedi la presentazione di Misko per i dettagli.

I componenti del modulo possono essere raggruppati in modo convenzionale per tipi come controllori, modelli, viste, filtri, direttive, ecc.

Ma il modulo stesso rimane riutilizzabile , trasferibile e verificabile .

È anche molto più facile per gli sviluppatori trovare alcune parti del codice e tutte le sue dipendenze.

Fare riferimento a Organizzazione del codice in Large AngularJS e Applicazioni JavaScript per i dettagli.

Un esempio di strutturazione delle cartelle :

 |-- src/ | |-- app/ | | |-- app.js | | |-- home/ | | | |-- home.js | | | |-- homeCtrl.js | | | |-- home.spec.js | | | |-- home.tpl.html | | | |-- home.less | | |-- user/ | | | |-- user.js | | | |-- userCtrl.js | | | |-- userModel.js | | | |-- userResource.js | | | |-- user.spec.js | | | |-- user.tpl.html | | | |-- user.less | | | |-- create/ | | | | |-- create.js | | | | |-- createCtrl.js | | | | |-- create.tpl.html | |-- common/ | | |-- authentication/ | | | |-- authentication.js | | | |-- authenticationModel.js | | | |-- authenticationService.js | |-- assets/ | | |-- images/ | | | |-- logo.png | | | |-- user/ | | | | |-- user-icon.png | | | | |-- user-default-avatar.png | |-- index.html 

Un buon esempio di strutturazione angular dell'applicazione è implementato da angular-app - https://github.com/angular-app/angular-app/tree/master/client/src

Questo è anche considerato dai moderni generatori di applicazioni - https://github.com/yeoman/generator-angular/issues/109

Credo che il punto di vista di Igor su questo, come visto nella citazione che hai fornito, sia solo la punta dell’iceberg di un problema molto più grande.

MVC ei suoi derivati ​​(MVP, PM, MVVM) sono tutti buoni e dandy all’interno di un singolo agente, ma un’architettura server-client è per tutti gli effetti un sistema a due agenti, e le persone sono così ossessionate da questi schemi che dimenticano che il problema è molto più complesso. Cercando di aderire a questi principi, in realtà finiscono con un’architettura imperfetta.

Facciamolo un po ‘alla volta.

Le linee guida

Visualizzazioni

All’interno del contesto angular, la vista è il DOM. Le linee guida sono:

Fare:

  • Variabile di ambito presente (sola lettura).
  • Chiama il controller per le azioni.

Non:

  • Metti qualsiasi logica.

Per quanto allettante, breve e innocuo, questo sembra:

 ng-click="collapsed = !collapsed" 

Significa praticamente qualsiasi sviluppatore che ora per capire come funziona il sistema di cui hanno bisogno per ispezionare sia i file Javascript che quelli HTML.

Controller

Fare:

  • Associare la vista al ‘modello’ inserendo i dati sull’oscilloscopio.
  • Rispondere alle azioni dell’utente.
  • Gestire la logica di presentazione.

Non:

  • Gestire qualsiasi logica aziendale.

Il motivo dell’ultima linea guida è che i controller sono sorelle per le visualizzazioni, non per le quadro; né sono riutilizzabili.

Si potrebbe sostenere che le direttive sono riutilizzabili, ma anche le direttive sono sorelle per le viste (DOM) – non sono mai state pensate per corrispondere alle entity framework.

Certo, a volte le visualizzazioni rappresentano quadro, ma questo è un caso piuttosto specifico.

In altre parole, i controllori devono concentrarsi sulla presentazione – se si introduce la logica aziendale, non solo si rischia di finire con un controllore gonfiato, poco gestibile, ma si viola anche il principio della separazione della preoccupazione .

In quanto tali, i controller in Angular sono molto più di Presentation Model o MVVM .

E quindi, se i controllori non dovessero occuparsi della logica aziendale, chi dovrebbe?

Cos’è un modello?

Il tuo modello cliente è spesso parziale e stantio

A meno che non si stia scrivendo un’applicazione web offline o un’applicazione estremamente semplice (poche quadro), è molto probabile che il modello client sia:

  • Parziale
    • O non ha tutte le quadro (come nel caso della paginazione)
    • Oppure non ha tutti i dati (come nel caso della paginazione)
  • Stantio : se il sistema ha più di un utente, in qualsiasi momento non è ansible essere sicuri che il modello in possesso del client sia uguale a quello che il server detiene.

Il modello reale deve persistere

Nel tradizionale MCV, il modello è l’unica cosa che viene mantenuta . Ogni volta che parliamo di modelli, questi devono essere mantenuti ad un certo punto. Il tuo cliente può manipolare i modelli a piacimento, ma fino a quando il viaggio di andata e ritorno verso il server non è stato completato con successo, il lavoro non viene eseguito.

conseguenze

I due punti sopra dovrebbero servire da precauzione: il modello che il cliente ha in mano può solo coinvolgere una logica di business parziale, per lo più semplice.

In quanto tale, è forse saggio, nel contesto del client, utilizzare la lettera minuscola M , quindi in realtà è mVC , mVP e mVVm . La grande M è per il server.

Logica di business

Forse uno dei concetti più importanti sui modelli di business è che puoi suddividerli in 2 tipi (io ometto il terzo view-business perché è una storia per un altro giorno):

  • Logica di dominio, ovvero le regole aziendali Enterprise , la logica indipendente dall’applicazione. Ad esempio, fornire un modello con proprietà firstName e sirName , un getter come getFullName() può essere considerato indipendente dall’applicazione.
  • Logica dell’applicazione, ovvero le regole aziendali dell’applicazione , che sono specifiche dell’applicazione. Ad esempio, i controlli degli errori e la gestione.

È importante sottolineare che entrambi questi aspetti all’interno di un contesto client non sono “reali” logiche di business – trattano solo la porzione di esso che è importante per il cliente. La logica dell’applicazione (non la logica del dominio) dovrebbe avere la responsabilità di facilitare la comunicazione con il server e la maggior parte dell’interazione dell’utente; mentre la logica del dominio è in gran parte su piccola scala, specifica delle quadro e guidata dalla presentazione.

La domanda rimane ancora – dove li si getta all’interno di un’applicazione angular?

Architettura a 3 o 4 livelli

Tutti questi framework MVW utilizzano 3 livelli:

Tre cerchi Inner - model, middle - controller, outer - view

Ma ci sono due problemi fondamentali con questo quando si tratta di clienti:

  • Il modello è parziale, stantio e non persiste.
  • Non c’è spazio per mettere la logica dell’applicazione.

Un’alternativa a questa strategia è la strategia a 4 livelli :

4 cerchi, dall'interno all'esterno: regole aziendali, regole di business applicativo, adattatori di interfaccia, framework e driver

Il vero affare qui è il livello delle regole di business applicativo (casi d’uso), che spesso va male ai clienti.

Questo livello è realizzato da interactors (Uncle Bob), che è praticamente ciò che Martin Fowler chiama un livello di servizio script di operazione .

Esempio concreto

Considera la seguente applicazione Web:

  • L’applicazione mostra un elenco impaginato di utenti.
  • L’utente fa clic su “Aggiungi utente”.
  • Un modello si apre con un modulo per riempire i dettagli dell’utente.
  • L’utente riempie il modulo e premi Invia.

Alcune cose dovrebbero accadere ora:

  • Il modulo deve essere convalidato dal client.
  • Una richiesta deve essere inviata al server.
  • Un errore deve essere gestito, se ce n’è uno.
  • L’elenco degli utenti può o non può (a causa di impaginazione) ha bisogno di aggiornamento.

Dove buttiamo tutto questo?

Se la tua architettura coinvolge un controller che chiama $resource , tutto ciò avverrà all’interno del controller. Ma c’è una strategia migliore.

Una soluzione proposta

Il seguente diagramma mostra come risolvere il problema precedente aggiungendo un altro livello logico dell’applicazione nei client Angular:

4 caselle: DOM punta a Controller, che punta alla logica dell'applicazione, che punta a $ risorsa

Quindi aggiungiamo un livello tra il controller a $ resource, questo layer (chiamiamolo interactor ):

  • È un servizio Nel caso degli utenti, può essere chiamato UserInteractor .
  • Fornisce metodi corrispondenti ai casi d’uso , incapsulando la logica dell’applicazione .
  • Controlla le richieste fatte al server. Invece di un controller che richiama $ risorse con parametri in formato libero, questo livello garantisce che le richieste fatte al server restituiscano dati su quale logica di dominio può agire.
  • Decora la struttura dei dati restituiti con il prototipo della logica di dominio .

E così, con i requisiti dell’esempio concreto di cui sopra:

  • L’utente fa clic su “Aggiungi utente”.
  • Il controller richiede all’interprete un modello utente vuoto, è decorato con il metodo della logica aziendale, come validate()
  • Al momento dell’invio, il controller chiama il metodo model validate() .
  • Se non funziona, il controller gestisce l’errore.
  • In caso di esito positivo, il controller chiama l’interactor con createUser()
  • L’interlocutore chiama $ resource
  • Dopo la risposta, l’interactor delega eventuali errori al controller, che li gestisce.
  • In caso di risposta positiva, l’interactor assicura che, se necessario, l’elenco degli utenti si aggiorni.

Un problema minore rispetto ai grandi consigli nella risposta di Artem, ma in termini di leggibilità del codice, ho trovato la migliore per definire completamente l’API all’interno dell’object return , per ridurre al minimo l’andare avanti e indietro nel codice per vedere le variabili di wheverer sono definite:

 angular.module('myModule', []) // or .constant instead of .value .value('myConfig', { var1: value1, var2: value2 ... }) .factory('myFactory', function(myConfig) { ...preliminary work with myConfig... return { // comments myAPIproperty1: ..., ... myAPImethod1: function(arg1, ...) { ... } } }); 

Se l’object di reso diventa “troppo affollato”, significa che il servizio sta facendo troppo.

AngularJS non implementa MVC in modo tradizionale, ma implementa qualcosa più vicino a MVVM (Model-View-ViewModel), ViewModel può anche essere indicato come binder (nel caso angular può essere $ scope). Il modello -> Come sappiamo, il modello in angular può essere semplicemente vecchi oggetti JS o i dati nella nostra applicazione

La vista -> la vista in angularJS è l’HTML che è stato analizzato e compilato da angularJS applicando le direttive o le istruzioni o le associazioni, il punto principale qui è in angular l’input non è solo la semplice stringa HTML (innerHTML), piuttosto è DOM creato dal browser.

ViewModel -> ViewModel è in realtà il raccoglitore / ponte tra la vista e il modello in caso angularJS è $ scope, per inizializzare e aumentare il $ scope che usiamo Controller.

Se voglio riassumere la risposta: Nell’applicazione angularJS $ scope ha riferimento ai dati, Controller controlla il comportamento e View gestisce il layout interagendo con il controller per comportarsi di conseguenza.

Per essere chiari sulla domanda, Angular utilizza diversi modelli di design che abbiamo già incontrato nella nostra programmazione regolare. 1) Quando registriamo i nostri controllori o direttive, fabbrica, servizi ecc. Rispetto al nostro modulo. Qui nasconde i dati dallo spazio globale. Qual è il modello del modulo . 2) Quando angular usa il suo controllo sporco per confrontare le variabili dell’oscilloscopio, qui usa il modello di osservazione . 3) Tutti gli ambiti figlio genitore nei nostri controllori utilizzano pattern Prototipale. 4) In caso di iniezione dei servizi utilizza Pattern di fabbrica .

Nel complesso utilizza diversi modelli di design noti per risolvere i problemi.