AngularJS: comprensione del modello di progettazione


147

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

MVC vs MVVM vs MVP . Che argomento controverso su cui molti sviluppatori possono passare ore e ore a discutere e discutere.

Per diversi anni AngularJS è stato più vicino a MVC (o meglio a una delle sue varianti lato client), ma nel tempo e grazie a molti refactoring e miglioramenti api, ora è più vicino a MVVM - l' oggetto $ scope potrebbe 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 sue API rendendo più semplice la creazione di un modello mentale che rappresenta l'applicazione che viene costruita con il framework. Può anche aiutare a stabilire la terminologia utilizzata dagli sviluppatori.

Detto questo, preferirei vedere gli sviluppatori costruire app di kick-ass che sono ben progettate e seguono la separazione delle preoccupazioni, piuttosto che vederle perdere tempo a discutere delle sciocchezze di MV *. E per questo motivo, dichiaro AngularJS come framework MVW - Model-View-qualunque . Dove Qualunque cosa significa " qualunque cosa funzioni per te ".

Angular offre molta flessibilità per separare in modo gradevole la logica di presentazione dalla logica aziendale e dallo stato di presentazione. Utilizzalo per alimentare la tua produttività e la manutenibilità dell'applicazione piuttosto che discussioni accese su cose che alla fine della giornata non contano molto.

Esistono raccomandazioni o linee guida per l'implementazione del modello di progettazione AngularJS MVW (Model-View-qualunque) nelle applicazioni lato client?


votato per ... che vederli perdere tempo a discutere di sciocchezze MV *.
Shirgill Farhan,

1
Non è necessario Angular per seguire un modello di progettazione di classe di parole.
utileBee

Risposte:


223

Grazie a un'enorme quantità di preziose fonti ho ricevuto alcuni consigli generali per l'implementazione dei componenti nelle app AngularJS:


controllore

  • Il controller dovrebbe essere solo uno strato intermedio tra modello e vista. Cerca di renderlo il più sottile possibile.

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

  • Il controller può comunicare con altri controller usando il metodo di invocazione (possibile quando i bambini vogliono comunicare con il genitore) o $ emettono , $ trasmettono e $ su metodi. I messaggi emessi e trasmessi dovrebbero essere ridotti al minimo.

  • Il controller non dovrebbe preoccuparsi della presentazione o della manipolazione del DOM.

  • Cerca di evitare i controller nidificati . In questo caso il controller principale viene interpretato come modello. Iniettare invece i modelli come servizi condivisi.

  • L'ambito del controller deve essere utilizzato per il modello di rilegatura con vista e
    incapsulamento di Visualizza modello come per il modello di progettazione del modello di presentazione.


Scopo

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

Quando si esegue l'associazione bidirezionale (modello ng), assicurarsi di non associarsi direttamente alle proprietà dell'ambito.


Modello

Il modello in AngularJS è un singleton definito dal servizio .

Il modello offre un modo eccellente per separare dati e visualizzazione.

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

  • Il modello dovrebbe essere considerato come un'implementazione di un'unità particolare. Si basa sul principio della responsabilità singola. L'unità è un'istanza responsabile del proprio ambito di logica correlata che può rappresentare una singola entità 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 in un'applicazione simile.

  • Isolando la logica dell'unità nel modello è stato reso più semplice individuare, aggiornare e gestire.

  • 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 delle dipendenze se non dipende realmente dalla riduzione dell'accoppiamento dei componenti e dalla testabilità e usabilità dell'unità .

  • Cerca di evitare l'uso dei listener di eventi nei modelli. Li rende più difficili da testare e generalmente uccide i modelli in termini di principio di responsabilità singola.

Implementazione del modello

Poiché il modello dovrebbe incapsulare alcune logiche in termini di dati e stato, dovrebbe limitare architettonicamente l'accesso ai suoi membri in modo da garantire un accoppiamento libero.

Il modo per farlo nell'applicazione AngularJS è definirlo usando il tipo di servizio di fabbrica . Questo ci consentirà di definire proprietà e metodi privati ​​molto facilmente e anche di restituire quelli accessibili al pubblico in un unico posto che lo renderà davvero 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
  };
});

Creazione di nuove istanze

Cerca di evitare di avere una fabbrica che restituisce una nuova funzione capace poiché questa inizia a interrompere l'iniezione di dipendenza e la libreria si comporterà in modo imbarazzante, specialmente per terze parti.

Un modo migliore per ottenere lo stesso risultato è utilizzare la factory come API per restituire una raccolta di oggetti con metodi getter e setter collegati ad essi.

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 possano essere iniettati nel controller e utilizzati nella tua vista.

In alcuni casi alcuni metodi richiedono l'accessibilità globale all'interno dell'applicazione. Per renderlo possibile è possibile definire la proprietà ' common ' in $ rootScope e associarla 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 della proprietà ' comune '. Questo è un qualche tipo di spazio dei nomi .

Ma non definire alcun metodo direttamente in $ rootScope . Ciò può comportare comportamenti imprevisti se utilizzato con la direttiva ngModel all'interno del proprio ambito di visualizzazione, in genere sporcare il proprio ambito e portare a metodi di ambito che prevalgono sui problemi.


Risorsa

Risorsa consente di interagire con diverse origini dati .

Dovrebbe essere implementato usando il principio della responsabilità singola .

In particolare, si tratta di un proxy riutilizzabile per gli endpoint HTTP / JSON.

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

Implementazione delle risorse

Una fabbrica che crea un oggetto risorsa che consente di interagire con origini dati lato server RESTful.

L'oggetto 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 , liberamente accoppiate e indipendenti.

I servizi sono una funzionalità che Angular offre alle app Web sul lato client dal lato server, dove i servizi sono stati comunemente utilizzati per molto tempo.

I servizi nelle app angolari sono oggetti sostituibili che sono collegati tra loro mediante l'iniezione delle dipendenze.

Angular viene fornito con diversi tipi di servizi. Ognuno con i propri casi d'uso. Per i dettagli, leggere Informazioni sui tipi di servizio .

Prova a considerare i principi fondamentali dell'architettura di servizio nella tua applicazione.

In generale secondo il 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 entità dei fornitori e delle entità 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 alla caratteristica / funzionalità o vista , non per tipo. Vedi la presentazione di Misko per i dettagli.

I componenti del modulo possono essere raggruppati convenzionalmente per tipi come controller, modelli, viste, filtri, direttive ecc.

Ma il modulo stesso rimane riutilizzabile , trasferibile e testabile .

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

Per i dettagli, fare riferimento a Organizzazione del codice in grandi applicazioni AngularJS e JavaScript .

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 di applicazioni angolari è 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


5
Ho una preoccupazione su: "Si consiglia vivamente di evitare la logica aziendale nel controller. Dovrebbe essere spostato sul modello". Tuttavia dalla documentazione ufficiale è possibile leggere: "In generale, un controller non dovrebbe tentare di fare troppo. Dovrebbe contenere solo la logica aziendale necessaria per una singola vista". stiamo parlando della stessa cosa?
op1ekun,

3
Direi: tratta Controller come View Model.
Artem Platonov,

1
+1. Alcuni ottimi consigli qui! 2. Sfortunatamente l'esempio di searchModelnon segue i consigli sulla riutilizzabilità. Sarebbe meglio importare le costanti tramite il constantservizio. 3. Qualche spiegazione che cosa si intende qui ?:Try to avoid having a factory that returns a new able function
Dmitri Zaitsev,

1
Anche la sovrascrittura della prototypeproprietà dell'oggetto rompe l'eredità, invece si può usareCar.prototype.save = ...
Dmitri Zaitsev

2
@ChristianAichinger, si tratta della natura della catena di prototipi JavaScript che ti costringe a utilizzare objectun'espressione di associazione bidirezionale per assicurarti di scrivere esattamente sulla proprietà o setterfunzione. Nel caso in cui si utilizzi la proprietà diretta del proprio ambito ( senza un punto ) si ha il rischio di nascondere la proprietà target desiderata con quella appena creata nell'ambito più vicino nella catena del prototipo quando si scrive su di essa. Questo è meglio spiegato nella presentazione di Misko
Artem Platonov,

46

Credo che la tesi di Igor su questo, come si vede nella citazione che hai fornito, è solo la punta dell'iceberg di un problema molto più grande.

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

Facciamolo poco a poco.

Le linee guida

Visualizzazioni

Nel contesto angolare, la vista è il DOM. Le linee guida sono:

Fare:

  • Variabile di portata attuale (sola lettura).
  • Chiama il controller per le azioni.

Non:

  • Metti qualsiasi logica.

Per quanto allettante, corto e innocuo questo aspetto:

ng-click="collapsed = !collapsed"

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

Controller

Fare:

  • Associare la vista al "modello" posizionando i dati sull'ambito.
  • Rispondi alle azioni dell'utente.
  • Gestire la logica di presentazione.

Non:

  • Gestire qualsiasi logica aziendale.

La ragione dell'ultima linea guida è che i controller sono sorelle delle opinioni, non entità; né sono riutilizzabili.

Si potrebbe sostenere che le direttive sono riutilizzabili, ma anche le direttive sono sorelle delle visioni (DOM): non sono mai state intese corrispondere alle entità.

Certo, a volte le viste rappresentano entità, ma questo è un caso piuttosto specifico.

In altre parole, i responsabili del trattamento si concentreranno sulla presentazione: se si introduce la logica aziendale, non solo si rischia di finire con un controllore gonfiato e poco gestibile, ma si viola anche il principio di separazione delle preoccupazioni .

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

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

Cos'è un modello?

Il tuo modello client è spesso parziale e stantio

A meno che tu non stia scrivendo un'applicazione web offline o un'applicazione che è terribilmente semplice (poche entità), è molto probabile che il tuo modello client sia:

  • Parziale
    • O non ha tutte le entità (come nel caso dell'impaginazione)
    • Oppure non ha tutti i dati (come nel caso dell'impaginazione)
  • Stantio : se il sistema ha più di un utente, in qualsiasi momento non si può essere certi che il modello in possesso del client sia lo stesso di quello in possesso del server.

Il vero modello deve persistere

Nella MCV tradizionale, il modello è l'unica cosa che si persiste . Ogni volta che parliamo di modelli, questi devono essere persistiti ad un certo punto. Il client può manipolare i modelli a piacimento, ma fino a quando il roundtrip al server non è stato completato correttamente, il lavoro non è stato completato.

conseguenze

I due punti sopra dovrebbero servire da cautela: il modello in possesso del cliente può comportare solo una logica aziendale parziale, per lo più semplice.

Come tale, è forse saggio, nel contesto del client, usare lettere minuscole M, quindi è davvero mVC , mVP e mVVm . Il grande Mè per il server.

Logica di business

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

  • Logica di dominio - alias Regole aziendali , la logica indipendente dall'applicazione. Ad esempio, dare un modello con firstNamee sirNameproprietà, un getter come getFullName()può essere considerato indipendente dall'applicazione.
  • Logica dell'applicazione - aka regole di business dell'applicazione , che è l'applicazione specifica. Ad esempio, controlli e gestione degli errori.

È importante sottolineare che entrambi nel contesto di un cliente non sono logiche di business "reali" , ma riguardano solo la parte di essa 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 delle interazioni dell'utente; mentre la logica del dominio è in gran parte su piccola scala, specifica dell'entità e basata sulla presentazione.

La domanda rimane ancora: dove li si lancia in un'applicazione angolare?

Architettura 3 vs 4 strati

Tutti questi framework MVW utilizzano 3 livelli:

Tre cerchi.  Modello interno, controller centrale, vista esterna

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

  • Il modello è parziale, stantio e non persiste.
  • Nessun posto dove mettere la logica dell'applicazione.

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

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

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

Questo livello è realizzato dagli interlocutori (Zio Bob), che è praticamente quello che Martin Fowler chiama un livello di servizio di script di operazioni .

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 compila il modulo e preme invia.

Alcune cose dovrebbero accadere ora:

  • Il modulo deve essere convalidato dal cliente.
  • Una richiesta deve essere inviata al server.
  • Deve essere gestito un errore, se presente.
  • L'elenco utenti potrebbe essere o meno aggiornato (a causa dell'impaginazione).

Dove buttiamo tutto questo?

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

Una soluzione proposta

Il diagramma seguente mostra come risolvere il problema sopra aggiungendo un altro livello logico dell'applicazione nei client angolari:

4 caselle - DOM punta al controller, che punta alla logica dell'applicazione, che punta a $ risorsa

Quindi aggiungiamo un livello tra controller a $ risorsa, questo livello (chiamiamolo interattore ):

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

E così, con i requisiti dell'esempio concreto sopra:

  • L'utente fa clic su "Aggiungi utente".
  • Il controller chiede all'interattatore un modello utente vuoto, il è decorato con il metodo della logica aziendale, come validate()
  • Al momento dell'invio, il controller chiama il validate()metodo del modello .
  • Se fallito, il controller gestisce l'errore.
  • Se ha esito positivo, il controller chiama l'interactactor con createUser()
  • L'interattatore chiama $ resource
  • Alla risposta, l'interattore delega eventuali errori al controller, che li gestisce.
  • In caso di risposta corretta, l'interactactor assicura che, se necessario, l'elenco degli utenti si aggiorna.

Quindi AngularJS è definito MVW (dove W è per qualsiasi cosa) poiché posso scegliere di avere un controller (con tutta la logica aziendale in esso) o un modello / presentatore vista (senza logica aziendale ma solo un po 'di codice per riempire la vista) con BL in un servizio separato? Ho ragione?
BAD_SEED

Migliore risposta. Hai un vero esempio su GitHub di un'app angolare a 4 livelli?
RPallas,

1
@RPallas, No, no (vorrei avere tempo per questo). Attualmente stiamo provando un'architettura in cui la "logica dell'applicazione" è solo un interattore al contorno; un resolver tra esso e il controller e un modello di vista con una logica di vista. Stiamo ancora sperimentando, quindi non il 100% dei pro o dei contro. Ma una volta fatto spero di scrivere un blog da qualche parte.
Izhaki,

1
@heringer Fondamentalmente, abbiamo introdotto modelli: costrutti OOP che rappresentano entità di dominio. Sono questi modelli che comunicano con le risorse, non con i controller. Incapsulano la logica del dominio. I controller chiamano i modelli, che a loro volta chiamano risorse.
Izhaki,

1
@ alex440 No. Anche se sono passati due mesi che un serio post sul blog su questo argomento è sulla punta delle mie dita. Natale sta arrivando - forse allora.
Izhaki,

5

Un problema minore rispetto ai grandi consigli nella risposta di Artem, ma in termini di leggibilità del codice, ho trovato il modo migliore per definire l'API completamente all'interno returndell'oggetto, per ridurre al minimo andare avanti e indietro nel codice per sembrare che le variabili siano 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' returnoggetto diventa "troppo affollato", significa che il Servizio sta facendo troppo.


0

AngularJS non implementa MVC in modo tradizionale, piuttosto implementa qualcosa di più vicino a MVVM (Model-View-ViewModel), ViewModel può anche essere indicato come raccoglitore (nel caso angolare può essere $ scope). Il modello -> Come sappiamo, il modello in angolare può essere semplicemente vecchi oggetti JS o 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 angolare 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 nel caso angularJS è $ scope, per inizializzare e aumentare l'ambito $ che utilizziamo Controller.

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


-1

Per essere chiari sulla domanda, Angular utilizza diversi modelli di progettazione che abbiamo già riscontrato nella nostra normale programmazione. 1) Quando registriamo i nostri controller o direttive, fabbrica, servizi ecc. Rispetto al nostro modulo. Qui nasconde i dati dallo spazio globale. Qual è il modello del modulo . 2) Quando angolare usa il suo controllo sporco per confrontare le variabili dell'oscilloscopio, qui usa il modello di osservatore . 3) Tutti gli ambiti figlio padre nei nostri controller utilizzano il modello prototipo. 4) In caso di iniezione dei servizi utilizza Factory Pattern .

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.