it-swarm.com.de

AngularJS: Designmuster verstehen

Im Zusammenhang mit diesem Beitrag von Igor Minar, Leitung von AngularJS: 

MVC vs MVVM vs MVP. Was für ein kontroverses Thema, das viele Entwickler kann stundenlang stundenlang debattieren und streiten.

Seit einigen Jahren war AngularJS näher an MVC (oder eher einer seiner Clientseitigen Varianten), jedoch im Laufe der Zeit und dank vieler Umgestaltungen und api Verbesserungen, es ist jetzt näher an MVVM - dem $ scope Objekt. könnte als ViewModel betrachtet werden, das von einem .__ dekoriert wird. Funktion, die wir ein Controller nennen.

Ein Framework kategorisieren und in einen der MV * -Buckets einfügen zu können, hat einige Vorteile . Es kann Entwicklern helfen, sich mit seiner apis vertraut zu machen, indem sie es einfacher, ein mentales Modell zu erstellen, das die Anwendung darstellt, die wird mit dem Rahmen gebaut. Es kann auch helfen, Terminologie, die von Entwicklern verwendet wird.

Ich möchte lieber sehen, dass Entwickler Kick-Ass-Apps erstellen, die .__ sind. gut gestaltet und folgt der Trennung der Bedenken, als sie als Abfall betrachten Zeit streiten über MV * Unsinn. Aus diesem Grund erkläre ich hiermit AngularJS soll MVW-Framework sein - Model-View-Whatever. Wo auch immer. steht für "was für Sie funktioniert".

Angular gibt Ihnen viel Flexibilität, um die Präsentation schön zu trennen Logik aus Geschäftslogik und Präsentationsstatus. Bitte verwenden Sie es Kraftstoff Ihre Produktivität und die Wartbarkeit Ihrer Anwendung, anstatt erhitzt zu werden Diskussionen über Dinge, die am Ende des Tages egal sind. viel.

Gibt es Empfehlungen oder Richtlinien für die Implementierung des AngularJS MVW (Model-View-Whatever) -Designmusters in clientseitigen Anwendungen?

146
Artem Platonov

Dank einer Vielzahl wertvoller Quellen habe ich einige allgemeine Empfehlungen für die Implementierung von Komponenten in AngularJS-Apps:


Regler

  • Der Controller sollte nur eine Zwischenschicht zwischen Modell und Ansicht sein. Versuchen Sie es als thin wie möglich zu machen.

  • Es wird dringend empfohlen, Geschäftslogik zu vermeiden im Controller. Es sollte zum Modell verschoben werden.

  • Der Controller kann mit anderen Controllern über einen Methodenaufruf kommunizieren (möglich, wenn Kinder mit den Eltern kommunizieren möchten) oder $ send , $ broadcast und $ on Methoden. Die gesendeten und gesendeten Nachrichten sollten auf ein Minimum beschränkt werden.

  • Der Controller sollte kümmert sich nicht um Präsentation oder DOM-Manipulation.

  • Versuchen Sie, geschachtelte Controller zu vermeiden . In diesem Fall wird der übergeordnete Controller als Modell interpretiert. Injizieren Sie stattdessen Modelle als Shared Services.

  • Scope in controller sollte für binding model with view and verwendet werden
    Kapselung View Model wie für Presentation Model Entwurfsmuster.


Umfang

Behandeln Sie den Gültigkeitsbereich als schreibgeschützt in Vorlagen und schreibgeschützt in Controllern . Der Zweck des Geltungsbereichs besteht darin, sich auf das Modell zu beziehen, nicht auf das Modell.

Stellen Sie beim bidirektionalen Binden (ng-model) sicher, dass Sie nicht direkt an die Bereichseigenschaften binden.


Modell

Das Modell in AngularJS ist ein singleton , definiert durch service .

Modell bietet eine hervorragende Möglichkeit, Daten und Anzeige zu trennen.

Modelle sind Hauptkandidaten für Unit-Tests, da sie in der Regel genau eine Abhängigkeit aufweisen (eine Art Ereignisemitter, im Allgemeinen $ rootScope ) und hoch testbares ) enthalten + Domänenlogik .

  • Das Modell sollte als Implementierung einer bestimmten Einheit betrachtet werden. Es basiert auf dem Prinzip der Einzelverantwortung. Unit ist eine Instanz, die für ihren eigenen Bereich verwandter Logik verantwortlich ist, die eine einzelne Entität in der realen Welt darstellen und sie in der Programmierwelt in Form von data und state beschreiben kann.

  • Das Modell sollte die Daten Ihrer Anwendung kapseln und ein API bereitstellen, um auf diese Daten zuzugreifen und sie zu bearbeiten.

  • Das Modell sollte portable sein, damit es problemlos zu einer ähnlichen Anwendung transportiert werden kann.

  • Durch das Isolieren der Einheitenlogik in Ihrem Modell haben Sie das Auffinden, Aktualisieren und Verwalten vereinfacht.

  • Model kann Methoden allgemeinerer globaler Modelle verwenden, die für die gesamte Anwendung gelten.

  • Versuchen Sie, die Komposition anderer Modelle in Ihrem Modell mithilfe der Abhängigkeitsinjektion zu vermeiden, wenn es nicht wirklich darauf ankommt, die Komponentenkopplung zu verringern und die Einheit zu erhöhen Testbarkeit und Benutzbarkeit .

  • Versuchen Sie, die Verwendung von Ereignis-Listenern in Modellen zu vermeiden. Dies erschwert das Testen und tötet Modelle im Allgemeinen im Sinne des Single-Responsibility-Prinzips.

Modellimplementierung

Da das Modell eine gewisse Logik in Bezug auf Daten und Status enthalten sollte, sollte es den Zugriff auf seine Mitglieder architektonisch einschränken, damit eine lose Kopplung gewährleistet werden kann.

In der AngularJS-Anwendung können Sie dies mithilfe des Factory-Service-Typs definieren. Auf diese Weise können wir sehr einfach private Eigenschaften und Methoden definieren und auch öffentlich zugängliche Eigenschaften an einer einzigen Stelle zurückgeben, sodass sie für Entwickler wirklich lesbar sind.

Ein Beispiel :

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
  };
});

Neue Instanzen erstellen

Vermeiden Sie es, eine Factory zu haben, die eine neue Funktionsfähigkeit zurückgibt, da dadurch die Abhängigkeitsinjektion unterbrochen wird und die Bibliothek sich insbesondere für Dritte unangenehm verhält.

Eine bessere Möglichkeit, dasselbe zu erreichen, besteht darin, die Factory als API zu verwenden, um eine Auflistung von Objekten mit daran angehängten Get- und Setter-Methoden zurückzugeben.

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
    // ...
  };
});

Globales Modell

Versuchen Sie im Allgemeinen, solche Situationen zu vermeiden und gestalten Sie Ihre Modelle ordnungsgemäß, damit sie in den Controller eingespeist und in Ihrer Ansicht verwendet werden können.

In bestimmten Fällen erfordern einige Methoden einen globalen Zugriff innerhalb der Anwendung. Um dies zu ermöglichen, können Sie die Eigenschaft ' common ' in $ rootScope definieren und an commonModel binden während des Bootstraps der Anwendung:

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

Alle Ihre globalen Methoden befinden sich in der Eigenschaft " common ". Dies ist eine Art Namespace .

Definieren Sie jedoch keine Methoden direkt in Ihrem $ rootScope . Dies kann zu nerwartetem Verhalten führen, wenn Sie mit der ngModel-Direktive in Ihrem Ansichtsbereich verwendet werden. Dies verschmutzt im Allgemeinen Ihren Bereich und führt dazu, dass Bereichsmethoden Probleme überschreiben.


Ressource

Mit Resource können Sie mit verschiedenen - Datenquellen interagieren.

Sollte implementiert werden mit single-responsibility-Prinzip .

In bestimmten Fällen handelt es sich um einen wiederverwendbaren Proxy für HTTP/JSON-Endpunkte.

Ressourcen werden in Modelle eingefügt und bieten die Möglichkeit, Daten zu senden/abzurufen.

Ressourcenimplementierung

Eine Factory, die ein Ressourcenobjekt erstellt, mit dem Sie mit RESTful-Server-Datenquellen interagieren können.

Das zurückgegebene Ressourcenobjekt verfügt über Aktionsmethoden, die Verhalten auf hoher Ebene bereitstellen, ohne dass eine Interaktion mit dem Service $ http auf niedriger Ebene erforderlich ist.


Dienstleistungen

Sowohl Modell als auch Ressource sind Services .

Dienste sind nicht zugeordnet, lose gekoppelte Funktionseinheiten, die in sich geschlossen sind.

Dienste sind eine Funktion, die Angular für clientseitige Webanwendungen von der Serverseite aus bereitstellt, auf der Dienste seit langer Zeit häufig verwendet werden.

Dienste in Angular Apps sind ersetzbare Objekte, die mithilfe der Abhängigkeitsinjektion miteinander verbunden werden.

Angular bietet verschiedene Arten von Diensten. Jeder mit seinen eigenen Anwendungsfällen. Bitte lesen Sie Grundlegendes zu Servicetypen für Details.

Versuchen Sie, Hauptprinzipien der Dienstarchitektur in Ihrer Anwendung zu berücksichtigen.

Im Allgemeinen laut Web Services Glossary :

Ein Dienst ist eine abstrakte Ressource, mit der Aufgaben ausgeführt werden können, die aus Sicht der Entitäten von Anbietern und Anforderern eine kohärente Funktionalität bilden. Zur Nutzung muss ein Service von einem konkreten Provider-Agenten realisiert werden.


Client-seitige Struktur

Im Allgemeinen wird die Clientseite der Anwendung in - Module aufgeteilt. Jedes Modul sollte testable als Einheit sein.

Versuchen Sie, Module in Abhängigkeit von Feature/Funktionalität oder View und nicht nach Typ zu definieren. Siehe Miskos Präsentation für Details.

Modulkomponenten können herkömmlicherweise nach Typen wie Steuerungen, Modellen, Ansichten, Filtern, Anweisungen usw. gruppiert werden.

Das Modul selbst bleibt jedoch wiederverwendbar , übertragbar und testbar .

Es ist für Entwickler auch viel einfacher, einige Teile des Codes und all seine Abhängigkeiten zu finden.

Weitere Informationen finden Sie unter Code-Organisation in Large AngularJS- und JavaScript-Anwendungen .

Ein Beispiel für die Strukturierung von Ordnern :

|-- 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

Ein gutes Beispiel für die angular -Anwendungsstrukturierung wird durch angle-app - https://github.com/angular-app/angular-app/tree/master/client/src

Dies wird auch von modernen Anwendungsgeneratoren berücksichtigt - https://github.com/yeoman/generator-angular/issues/109

223
Artem Platonov

Ich glaube, Igor nimmt das an, wie Sie in Ihrem Zitat gesehen haben, und ist nur die Spitze des Eisbergs eines weitaus größeren Problems.

[~ # ~] mvc [~ # ~] und seine Derivate (MVP, PM, MVVM) sind alle in einem einzigen Agenten gut und fehlerfrei, aber eine Server-Client-Architektur ist es für alle Zwecke ein Zwei-Agenten-System, und die Menschen sind oft so besessen von diesen Mustern, dass sie vergessen, dass das vorliegende Problem weitaus komplexer ist. Indem sie versuchen, diese Prinzipien einzuhalten, erhalten sie tatsächlich eine fehlerhafte Architektur.

Lassen Sie uns dies Stück für Stück tun.

Die Richtlinien

Ansichten

Im Kontext Angular ist die Ansicht das DOM. Die Richtlinien lauten:

Machen:

  • Aktuelle Bereichsvariable (schreibgeschützt).
  • Rufen Sie den Controller für Aktionen.

Nicht:

  • Setzen Sie eine Logik.

So verlockend, kurz und harmlos sieht das aus:

ng-click="collapsed = !collapsed"

Es bedeutet für jeden Entwickler, dass er jetzt verstehen muss, wie das System funktioniert, um sowohl die Javascript-Dateien als auch die HTML-Dateien zu überprüfen.

Controller

Machen:

  • Binden Sie die Ansicht an das 'Modell', indem Sie Daten auf dem Bereich platzieren.
  • Reagieren Sie auf Benutzeraktionen.
  • Beschäftige dich mit Präsentationslogik.

Nicht:

  • Beschäftigen Sie sich mit jeder Geschäftslogik.

Der Grund für die letzte Richtlinie ist, dass Controller Schwestern von Ansichten sind und keine Entitäten. noch sind sie wiederverwendbar.

Sie könnten argumentieren, dass Direktiven wiederverwendbar sind, aber auch Direktiven sind Sisters to Views (DOM) - sie sollten niemals Entitäten entsprechen.

Sicher, manchmal repräsentieren Ansichten Entitäten, aber das ist ein ziemlich spezieller Fall.

Mit anderen Worten, Controller müssen sich auf die Präsentation konzentrieren. Wenn Sie Geschäftslogik einsetzen, werden Sie wahrscheinlich nicht nur einen aufgeblasenen, wenig überschaubaren Controller haben, sondern auch gegen die Trennung der Bedenken Verstoßen = Prinzip.

Als solche sind Controller in Angular wirklich mehr von Presentation Model oder [~ # ~] mvvm [~ # ~] .

Und wenn sich Controller nicht mit Geschäftslogik befassen sollten, wer sollte das dann tun?

Was ist ein Modell?

Ihr Kundenmodell ist oft unvollständig und abgestanden

Wenn Sie keine Offline-Webanwendung oder eine schrecklich einfache Anwendung (nur wenige Entitäten) schreiben, ist Ihr Client-Modell höchstwahrscheinlich:

  • Teilweise
    • Entweder hat es nicht alle Entitäten (wie im Fall der Paginierung)
    • Oder es hat nicht alle Daten (wie im Fall der Paginierung)
  • Veraltet - Wenn das System mehr als einen Benutzer hat, können Sie zu keinem Zeitpunkt sicher sein, dass das Modell, das der Client besitzt, dasselbe ist wie das, das der Server besitzt.

Das reale Modell muss bestehen bleiben

Im traditionellen MCV wird nur das Modell beibehalten . Wann immer wir über Modelle sprechen, müssen diese irgendwann bestehen bleiben. Ihr Client kann Modelle nach Belieben manipulieren, aber bis der Roundtrip zum Server erfolgreich abgeschlossen wurde, ist die Aufgabe noch nicht erledigt.

Konsequenzen

Die beiden obigen Punkte sollten als Warnung dienen - das Modell, das Ihr Kunde hat, kann nur eine teilweise, meist einfache Geschäftslogik beinhalten.

Als solches ist es vielleicht ratsam, im Client-Kontext M in Kleinbuchstaben zu verwenden - es ist also wirklich mVC , mVP und mVVm . Das große M ist für den Server.

Geschäftslogik

Vielleicht ist eines der wichtigsten Konzepte bei Geschäftsmodellen, dass Sie sie in zwei Typen unterteilen können (ich lasse den dritten weg view-business, da dies eine Geschichte für einen anderen Tag ist):

  • Domänenlogik - aka Unternehmensgeschäftsregeln, die anwendungsunabhängige Logik. Geben Sie beispielsweise ein Modell mit den Eigenschaften firstName und sirName an. Ein Getter wie getFullName() kann als anwendungsunabhängig betrachtet werden.
  • Anwendungslogik - aka Geschäftsregeln für Anwendungen, die anwendungsspezifisch ist. Zum Beispiel Fehlerprüfungen und -behandlung.

Es ist wichtig zu betonen, dass beide innerhalb eines Kundenkontexts keine „echte“ Geschäftslogik sind - sie befassen sich nur mit dem Teil davon, der für den Kunden wichtig ist. Die Anwendungslogik (nicht die Domänenlogik) sollte dafür verantwortlich sein, die Kommunikation mit dem Server und die meisten Benutzerinteraktionen zu erleichtern. Die Domänenlogik ist weitgehend kleinräumig, entitätsspezifisch und präsentationsorientiert.

Die Frage bleibt noch - wo wirfst du sie in eine angular Anwendung?

3 gegen 4 Schichtarchitektur

Alle diese MVW-Frameworks verwenden drei Ebenen:

Three circles. Inner - model, middle - controller, outer - view

Aber es gibt zwei grundlegende Probleme, wenn es um Kunden geht:

  • Das Modell ist partiell, abgestanden und besteht nicht.
  • Kein Platz für Anwendungslogik.

Eine Alternative zu dieser Strategie ist die 4-Ebenen-Strategie :

4 circles, from inner to outer - Enterprise business rules, Application business rules, Interface adapters, Frameworks and drivers

Das eigentliche Problem hierbei ist die Ebene der Anwendungsgeschäftsregeln (Use Cases), die den Kunden häufig missfällt.

Diese Schicht wird von Interaktoren (Onkel Bob) realisiert, was Martin Fowler als Operationsskript-Service-Schicht bezeichnet.

Konkretes Beispiel

Betrachten Sie die folgende Webanwendung:

  • Die Anwendung zeigt eine paginierte Liste von Benutzern.
  • Der Benutzer klickt auf "Benutzer hinzufügen".
  • Ein Modell wird mit einem Formular zum Ausfüllen von Benutzerdetails geöffnet.
  • Der Benutzer füllt das Formular aus und drückt auf Senden.

Ein paar Dinge sollten jetzt passieren:

  • Das Formular sollte vom Kunden validiert sein.
  • Eine Anfrage wird an den Server gesendet.
  • Ein Fehler soll behandelt werden, wenn es einen gibt.
  • Die Benutzerliste muss möglicherweise (aufgrund der Paginierung) aktualisiert werden.

Wo werfen wir das alles hin?

Wenn Ihre Architektur einen Controller umfasst, der $resource Aufruft, geschieht dies alles innerhalb des Controllers. Aber es gibt eine bessere Strategie.

Eine vorgeschlagene Lösung

Das folgende Diagramm zeigt, wie das obige Problem durch Hinzufügen einer weiteren Anwendungslogikebene in Angular Clients gelöst werden kann:

4 boxes - DOM points to Controller, which points to Application logic, which points to $resource

Also fügen wir eine Ebene zwischen controller und $ resource hinzu, diese Ebene (nennen wir es interactor):

  • Ist ein Dienst . Im Fall von Benutzern kann es UserInteractor genannt werden.
  • Es werden Methoden bereitgestellt, die Anwendungsfällen und entsprechen, die die Anwendungslogik einkapseln.
  • Es steuert die an den Server gestellten Anforderungen. Anstelle eines Controllers, der $ resource mit Freiformparametern aufruft, stellt diese Ebene sicher, dass an den Server gesendete Anforderungen Daten zurückgeben, auf die die Domänenlogik angewendet werden kann.
  • Es verziert die zurückgegebene Datenstruktur mit einem Domänenlogikprototyp .

Und so, mit den Anforderungen des obigen konkreten Beispiels:

  • Der Benutzer klickt auf "Benutzer hinzufügen".
  • Der Controller fragt den Interaktor nach einem leeren Benutzermodell, das mit einer Geschäftslogik-Methode wie validate() dekoriert ist.
  • Bei der Übermittlung ruft der Controller die Methode model validate() auf.
  • Wenn dies fehlschlägt, behandelt der Controller den Fehler.
  • Bei Erfolg ruft der Controller den Interaktor mit createUser() auf.
  • Der Interaktor ruft $ resource auf
  • Bei einer Antwort delegiert der Interaktor alle Fehler an den Controller, der sie behandelt.
  • Nach einer erfolgreichen Antwort stellt der Interaktor sicher, dass die Benutzerliste bei Bedarf aktualisiert wird.
46
Izhaki

Ein kleineres Problem im Vergleich zu den großartigen Ratschlägen in Artems Antwort, aber im Hinblick auf die Lesbarkeit des Codes fand ich es am besten, die API vollständig innerhalb des return-Objekts zu definieren, um das Hin- und Hergehen im Code zu minimieren, wenn Variablen definiert werden: 

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, ...) {
    ...
    }
  }
});

Wenn das return-Objekt "zu voll" erscheint, ist dies ein Zeichen dafür, dass der Dienst zu viel tut.

5
Dmitri Zaitsev

AngularJS implementiert MVC nicht auf herkömmliche Weise, sondern implementiert etwas näher an MVVM (Model-View-ViewModel), ViewModel kann auch als Binder bezeichnet werden (im Winkel kann es $ scope sein) . Das Model -> Wie wir wissen, kann das Modell in eckig einfach nur alte JS-Objekte oder die Daten in unserer Anwendung sein

Die Ansicht -> Die Ansicht in angleJS ist der HTML-Code, der von angleJS durch Anwenden der Direktiven oder Anweisungen oder Bindungen analysiert und kompiliert wurde. Der Hauptpunkt hier in Winkel ist die Eingabe nicht nur der reine HTML-String (innerHTML), sondern er ist das vom Browser erstellte DOM.

Das ViewModel -> ViewModel ist eigentlich die Bindungslinie zwischen Ansicht und Modell in FallJS, es ist $ scope, um den $ scope, den wir verwenden, zu initialisieren und zu erweitern.

Wenn ich die Antwort zusammenfassen möchte: In angleJS hat $ scope einen Verweis auf die Daten, der Controller steuert das Verhalten und View behandelt das Layout durch Interaktion mit dem Controller, um sich entsprechend zu verhalten.

0
Ashutosh