Wo werden Modelldaten und -verhalten abgelegt? [tl; DR; Dienste nutzen]

341

Ich arbeite mit AngularJS für mein aktuelles Projekt. In der Dokumentation und in den Tutorials werden alle Modelldaten in den Controller-Bereich gestellt. Ich verstehe, dass es da sein muss, um für den Controller und damit in den entsprechenden Ansichten verfügbar zu sein.

Ich denke jedoch nicht, dass das Modell dort tatsächlich implementiert werden sollte. Es kann komplex sein und beispielsweise private Attribute haben. Außerdem möchte man es möglicherweise in einem anderen Kontext / einer anderen App wiederverwenden. Wenn Sie alles in den Controller einfügen, wird das MVC-Muster vollständig zerstört.

Gleiches gilt für das Verhalten eines Modells. Wenn ich eine DCI-Architektur verwenden und das Verhalten vom Datenmodell trennen würde, müsste ich zusätzliche Objekte einführen, um das Verhalten zu halten. Dies würde durch die Einführung von Rollen und Kontexten geschehen.

DCI == D ata C ollaboration I nteraction

Natürlich könnten Modelldaten und -verhalten mit einfachen Javascript-Objekten oder einem beliebigen "Klassen" -Muster implementiert werden. Aber wie würde AngularJS das machen? Dienste nutzen?

Es kommt also auf diese Frage an:

Wie implementieren Sie vom Controller entkoppelte Modelle gemäß den Best Practices von AngularJS?

Nils Blum-Oeste
quelle
12
Ich würde diese Frage positiv bewerten, wenn Sie DCI definieren oder zumindest das formulierte Formular bereitstellen könnten. Ich habe dieses Akronym in keiner Softwareliteratur gesehen. Vielen Dank.
Jim Raden
13
Ich habe gerade einen Link für DCI als Referenz hinzugefügt.
Nils Blum-Oeste
1
@JimRaden DCI ist Dataq, Context, Interaction und ein Paradigma, das zuerst vom Vater von MVC (Trygve Reenskauge) formuliert wurde. Zu diesem Thema gibt es inzwischen einige Literatur. Eine gute Lektüre ist Coplien und Bjørnvig "Lean Architecture"
Rune FS
3
Vielen Dank. Ob gut oder schlecht, die meisten Menschen wissen noch nicht einmal über die Originalliteratur Bescheid. Laut Google gibt es 55 Millionen Artikel über MVC, aber nur 250.000, in denen MCI und MVC erwähnt werden. Und auf Microsoft.com? 7. AngularJS.org erwähnt nicht einmal das DCI-Akronym: "Ihre Suchseite: angularjs.org dci - stimmte nicht mit Dokumenten überein".
Jim Raden
Ressourcenobjekte sind im Grunde die Modelle in Angular.js. Ich erweitere sie.
Salman von Abbas

Antworten:

155

Sie sollten Dienste verwenden, wenn Sie etwas möchten, das von mehreren Controllern verwendet werden kann. Hier ist ein einfaches Beispiel:

myApp.factory('ListService', function() {
  var ListService = {};
  var list = [];
  ListService.getItem = function(index) { return list[index]; }
  ListService.addItem = function(item) { list.push(item); }
  ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
  ListService.size = function() { return list.length; }

  return ListService;
});

function Ctrl1($scope, ListService) {
  //Can add/remove/get items from shared list
}

function Ctrl2($scope, ListService) {
  //Can add/remove/get items from shared list
}
Andrew Joslin
quelle
23
Was wäre der Vorteil der Verwendung eines Dienstes gegenüber der Erstellung eines einfachen Javascript-Objekts als Modell und dessen Zuordnung zum Controller-Bereich?
Nils Blum-Oeste
22
Falls Sie dieselbe Logik benötigen, die von mehreren Controllern gemeinsam genutzt wird. Auf diese Weise ist es auch einfacher, Dinge unabhängig zu testen.
Andrew Joslin
1
Das letzte Beispiel Art gesaugt, dieses macht mehr Sinn. Ich habe es bearbeitet.
Andrew Joslin
9
Ja, mit einem einfachen alten Javascript-Objekt könnten Sie Angular nicht in Ihren ListService einfügen. Wie in diesem Beispiel, wenn Sie $ http.get benötigen, um die Listendaten zu Beginn abzurufen, oder wenn Sie $ rootScope injizieren müssen, damit Sie $ Ereignisse senden können.
Andrew Joslin
1
Sollten sich die Daten nicht außerhalb von ListService befinden, um dieses Beispiel DCI-ähnlicher zu gestalten?
PiTheNumber
81

Ich versuche derzeit dieses Muster, das, obwohl nicht DCI, eine klassische Dienst- / Modellentkopplung bietet (mit Diensten für die Kommunikation mit Webdiensten (auch bekannt als Modell CRUD) und einem Modell, das die Objekteigenschaften und -methoden definiert).

Beachten Sie, dass ich dieses Muster nur verwende, wenn das Modellobjekt Methoden benötigt, die an seinen eigenen Eigenschaften arbeiten, die ich wahrscheinlich überall verwenden werde (z. B. verbesserte Getter / Setter). Ich bin nicht systematisch tun dies für jeden Dienst befürworten.

EDIT: Früher dachte ich, dieses Muster würde gegen das Mantra "Winkelmodell ist einfaches altes Javascript-Objekt" verstoßen, aber jetzt scheint es mir, dass dieses Muster vollkommen in Ordnung ist.

EDIT (2): Um noch klarer zu sein, verwende ich eine Model-Klasse nur, um einfache Getter / Setter zu berücksichtigen (z. B. zur Verwendung in Ansichtsvorlagen). Für die Logik großer Unternehmen empfehle ich die Verwendung separater Dienste, die das Modell "kennen", aber von diesen getrennt bleiben und nur Geschäftslogik enthalten. Nennen Sie es eine "Business Expert" -Service-Schicht, wenn Sie möchten

service / ElementServices.js (beachten Sie, wie Element in die Deklaration eingefügt wird)

MyApp.service('ElementServices', function($http, $q, Element)
{
    this.getById = function(id)
    {
        return $http.get('/element/' + id).then(
            function(response)
            {
                //this is where the Element model is used
                return new Element(response.data);
            },
            function(response)
            {
                return $q.reject(response.data.error);
            }
        );
    };
    ... other CRUD methods
}

model / Element.js (mit anglejs Factory, erstellt für die Objekterstellung)

MyApp.factory('Element', function()
{
    var Element = function(data) {
        //set defaults properties and functions
        angular.extend(this, {
            id:null,
            collection1:[],
            collection2:[],
            status:'NEW',
            //... other properties

            //dummy isNew function that would work on two properties to harden code
            isNew:function(){
                return (this.status=='NEW' || this.id == null);
            }
        });
        angular.extend(this, data);
    };
    return Element;
});
Ben G.
quelle
4
Ich steige gerade in Angular ein, aber ich wäre gespannt, ob / warum die Veteranen dies für Häresie halten würden. Dies ist wahrscheinlich die Art und Weise, wie ich es anfangs auch angehen würde. Könnte jemand ein Feedback geben?
Aaronius
2
@Aaronius nur um klar zu sein: Ich habe noch nie in einem AngularJS-Dokument oder Blog gelesen, dass man das niemals tun sollte, aber ich habe immer Dinge wie "AngularJs braucht kein Modell, es wird nur einfaches altes Javascript verwendet" gelesen. und ich musste dieses Muster selbst entdecken. Da dies mein erstes echtes Projekt auf AngularJS ist, setze ich diese starken Warnungen, damit die Leute nicht kopieren / einfügen, ohne vorher darüber nachzudenken.
Ben G
Ich habe mich für ein ungefähr ähnliches Muster entschieden. Es ist eine Schande, dass Angular keine wirkliche Unterstützung (oder scheinbar den Wunsch zu unterstützen) eines Modells im "klassischen" Sinne hat.
drt
3
Das sieht für mich nicht nach einer Häresie aus. Sie verwenden Fabriken für das, wofür sie geschaffen wurden: Objekte bauen. Ich glaube, der Ausdruck "anglejs braucht kein Modell" bedeutet "Sie müssen nicht von einer speziellen Klasse erben oder spezielle Methoden (wie ko.observable, in Knockout) verwenden, um mit Modellen in Angular zu arbeiten, a reines js Objekt wird ausreichen ".
Felipe Castro
1
Würde ein entsprechend benannter ElementService nicht für jede Sammlung zu einer Reihe nahezu identischer Dateien führen?
Collin Allen
29

In der Angularjs-Dokumentation heißt es eindeutig:

Im Gegensatz zu vielen anderen Frameworks stellt Angular keine Einschränkungen oder Anforderungen an das Modell. Es gibt keine Klassen zum Erben oder spezielle Zugriffsmethoden für den Zugriff auf oder das Ändern des Modells. Das Modell kann primitiv, Objekt-Hash oder ein vollständiger Objekttyp sein. Kurz gesagt, das Modell ist ein einfaches JavaScript-Objekt.

- AngularJS Developer Guide - V1.5-Konzepte - Modell

Es liegt also an Ihnen, wie Sie ein Modell deklarieren. Es ist ein einfaches Javascript-Objekt.

Ich persönlich werde Angular Services nicht verwenden, da sie sich wie Singleton-Objekte verhalten sollen, mit denen Sie beispielsweise globale Zustände in Ihrer Anwendung beibehalten können.

SC
quelle
Sie sollten einen Link angeben, wo dies in der Dokumentation angegeben ist. Ich habe eine Google-Suche nach "Angular stellt keine Einschränkungen oder Anforderungen an das Modell" durchgeführt und sie taucht, soweit ich das beurteilen kann, nirgendwo in den offiziellen Dokumenten auf.
4
Es war in den alten Angularjs-Dokumenten (die während der Antwort noch am Leben waren): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
SC
8

DCI ist ein Paradigma und als solches gibt es keine eckige Methode, entweder die Sprachunterstützung DCI oder nicht. JS unterstützt DCI ziemlich gut, wenn Sie bereit sind, die Quellentransformation zu verwenden, und mit einigen Nachteilen, wenn Sie dies nicht tun. Wiederum hat DCI nicht mehr mit Abhängigkeitsinjektion zu tun, als zu sagen, dass eine C # -Klasse dies getan hat und definitiv auch kein Dienst ist. Der beste Weg, DCI mit AngulusJS zu machen, ist also, DCI auf JS-Weise zu machen, was ziemlich nahe daran liegt, wie DCI überhaupt formuliert wird. Wenn Sie keine Quellentransformation durchführen, können Sie dies nicht vollständig tun, da die Rollenmethoden auch außerhalb des Kontexts Teil des Objekts sind. Dies ist jedoch im Allgemeinen das Problem bei DCI mit Methodeninjektion. Wenn Sie sich fullOO.info ansehenAuf der maßgeblichen Website für DCI können Sie sich die Ruby-Implementierungen ansehen, bei denen auch die Methodeninjektion verwendet wird, oder Sie können sich hier weitere Informationen zu DCI ansehen . Es ist meistens mit RUby-Beispielen, aber das DCI-Zeug ist dafür agnostisch. Einer der Schlüssel zu DCI ist, dass das, was das System tut, von dem, was das System ist, getrennt ist. Das Datenobjekt ist also ziemlich dumm, aber sobald es an eine Rolle in einem Kontext gebunden ist, stellen Rollenmethoden ein bestimmtes Verhalten zur Verfügung. Eine Rolle ist einfach ein Bezeichner, nichts weiter. Wenn Sie über diesen Bezeichner auf ein Objekt zugreifen, stehen Rollenmethoden zur Verfügung. Es gibt kein Rollenobjekt / keine Rollenklasse. Bei der Methodeninjektion ist der Umfang der Rollenmethoden nicht genau wie beschrieben, sondern eng. Ein Beispiel für einen Kontext in JS könnte sein

function transfer(source,destination){
   source.transfer = function(amount){
        source.withdraw(amount);
        source.log("withdrew " + amount);
        destination.receive(amount);
   };
   destination.receive = function(amount){
      destination.deposit(amount);
      destination.log("deposited " + amount);
   };
   this.transfer = function(amount){
    source.transfer(amount);
   };
}
Rune FS
quelle
1
Vielen Dank für die Ausarbeitung des DCI-Materials. Es ist eine großartige Lektüre. Aber meine Fragen zielen wirklich darauf ab, "wo die Modellobjekte in Winkeljs platziert werden sollen". DCI dient nur als Referenz, damit ich nicht nur ein Modell habe, sondern es auf DCI-Weise aufteile. Bearbeitet die Frage, um sie klarer zu machen.
Nils Blum-Oeste
7

Dieser Artikel über Modelle in AngularJS könnte helfen:

http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/

Marianboda
quelle
7
Beachten Sie, dass von Antworten nur mit Links abgeraten wird. SO-Antworten sollten der Endpunkt einer Suche nach einer Lösung sein (im Vergleich zu einem weiteren Zwischenstopp von Referenzen, die im Laufe der Zeit veralten). Bitte fügen Sie hier eine eigenständige Zusammenfassung hinzu, wobei Sie den Link als Referenz behalten.
Kleopatra
Das Hinzufügen eines solchen Links in einem Kommentar zur Frage wäre jedoch in Ordnung.
Jorrebor
Dieser Link ist eigentlich ein sehr guter Artikel, aber ebenso müsste er zu einer Antwort verarbeitet werden, um für SO geeignet zu sein
Jeremy Zerr
5

Wie auf anderen Postern angegeben, bietet Angular keine sofort einsatzbereite Basisklasse für die Modellierung, es können jedoch mehrere Funktionen bereitgestellt werden:

  1. Methoden zur Interaktion mit einer RESTful-API und zum Erstellen neuer Objekte
  2. Beziehungen zwischen Modellen herstellen
  3. Überprüfen der Daten vor dem Fortbestehen im Backend; Auch nützlich für die Anzeige von Echtzeitfehlern
  4. Caching und Lazy-Loading, um keine verschwenderischen HTTP-Anfragen zu stellen
  5. Hooks für Zustandsmaschinen (vor / nach dem Speichern, Aktualisieren, Erstellen, Neuem usw.)

Eine Bibliothek, die all diese Dinge gut macht, ist ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource) ). Vollständige Offenlegung - ich habe diese Bibliothek geschrieben - und ich habe sie erfolgreich beim Erstellen mehrerer Anwendungen im Unternehmensmaßstab verwendet. Es ist gut getestet und bietet eine API, die Rails-Entwicklern vertraut sein sollte.

Mein Team und ich entwickeln diese Bibliothek weiterhin aktiv weiter, und ich würde gerne sehen, dass mehr Angular-Entwickler dazu beitragen und sie im Kampf testen.

Brett Cassette
quelle
Hallo! Das ist wirklich toll! Ich werde es jetzt in meine App einstecken. Die Kampftests haben gerade begonnen.
J. Bruni
1
Ich habe mir nur Ihren Beitrag angesehen und mich gefragt, was die Unterschiede zwischen Ihrem ngActiveResourceund Angulars $resourceService sind. Ich bin ein wenig neu in Angular und habe beide Dokumentensätze schnell durchsucht, aber sie scheinen viel Überlappung zu bieten. Wurde ngActiveResourceentwickelt, bevor der $resourceService verfügbar war?
Eric B.
5

Eine ältere Frage, aber ich denke, das Thema ist angesichts der neuen Ausrichtung von Angular 2.0 relevanter als je zuvor. Ich würde sagen, eine bewährte Methode besteht darin, Code mit möglichst wenigen Abhängigkeiten von einem bestimmten Framework zu schreiben. Verwenden Sie die Framework-spezifischen Teile nur dort, wo sie einen direkten Mehrwert bieten.

Derzeit scheint der Angular-Dienst eines der wenigen Konzepte zu sein, die es in die nächste Generation von Angular schaffen. Daher ist es wahrscheinlich klug, die allgemeine Richtlinie zum Verschieben der gesamten Logik auf Dienste zu befolgen. Ich würde jedoch argumentieren, dass Sie entkoppelte Modelle auch ohne direkte Abhängigkeit von Angular-Diensten erstellen können. Das Erstellen eigenständiger Objekte mit nur den erforderlichen Abhängigkeiten und Verantwortlichkeiten ist wahrscheinlich der richtige Weg. Es macht das Leben auch viel einfacher, wenn automatisierte Tests durchgeführt werden. Einzelverantwortung ist heutzutage eine Modeerscheinung, aber es macht sehr viel Sinn!

Hier ist ein Beispiel für ein Muster, das ich für gut halte, um das Objektmodell vom Dom zu entkoppeln.

http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

Ein wichtiges Ziel ist es, Ihren Code so zu strukturieren, dass er aus Unit-Tests genauso einfach zu verwenden ist wie aus einer Ansicht. Wenn Sie dies erreichen, sind Sie gut positioniert, um realistische und nützliche Tests zu schreiben.

TGH
quelle
4

Ich habe versucht, genau dieses Problem in diesem Blog-Beitrag anzugehen .

Grundsätzlich ist das beste Zuhause für die Datenmodellierung in Diensten und Fabriken. Abhängig davon, wie Sie Ihre Daten abrufen und wie komplex die von Ihnen benötigten Verhaltensweisen sind, gibt es viele verschiedene Möglichkeiten, die Implementierung durchzuführen. Angular hat derzeit keine Standardmethode oder Best Practice.

Der Beitrag behandelt drei Ansätze: $ http , $ resource und Restangular .

Hier ist ein Beispielcode für jeden mit einem benutzerdefinierten Code getResult() Methode für das Jobmodell:

Restangular (leicht peasy):

angular.module('job.models', [])
  .service('Job', ['Restangular', function(Restangular) {
    var Job = Restangular.service('jobs');

    Restangular.extendModel('jobs', function(model) {
      model.getResult = function() {
        if (this.status == 'complete') {
          if (this.passed === null) return "Finished";
          else if (this.passed === true) return "Pass";
          else if (this.passed === false) return "Fail";
        }
        else return "Running";
      };

      return model;
    });

    return Job;
  }]);

$ resource (etwas komplizierter):

angular.module('job.models', [])
    .factory('Job', ['$resource', function($resource) {
        var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: function(data, header) {
                    var wrapped = angular.fromJson(data);
                    angular.forEach(wrapped.items, function(item, idx) {
                        wrapped.items[idx] = new Job(item);
                    });
                    return wrapped;
                }
            }
        });

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    }]);

$ http (Hardcore):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .factory('Job', function() {
        function Job(data) {
            for (attr in data) {
                if (data.hasOwnProperty(attr))
                    this[attr] = data[attr];
            }
        }

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    });

Der Blog-Beitrag selbst geht detaillierter auf die Gründe ein, warum Sie die einzelnen Ansätze verwenden könnten, sowie auf Codebeispiele für die Verwendung der Modelle in Ihren Controllern:

AngularJS-Datenmodelle: $ http VS $ resource VS Restangular

Es besteht die Möglichkeit, dass Angular 2.0 eine robustere Lösung für die Datenmodellierung bietet, mit der alle auf dieselbe Seite gelangen.

Alan Christopher Thomas
quelle