Où mettre les données et le comportement du modèle? [tl; dr; Utiliser les services]

341

Je travaille avec AngularJS pour mon dernier projet. Dans la documentation et les didacticiels, toutes les données de modèle sont placées dans la portée du contrôleur. Je comprends que cela doit être là pour être disponible pour le contrôleur et donc dans les vues correspondantes.

Cependant, je ne pense pas que le modèle devrait réellement être mis en œuvre là-bas. Il peut être complexe et avoir des attributs privés par exemple. De plus, on peut vouloir le réutiliser dans un autre contexte / application. Tout mettre dans le contrôleur rompt totalement le modèle MVC.

Il en va de même pour le comportement de tout modèle. Si j'utilisais l' architecture DCI et un comportement distinct du modèle de données, je devrais introduire des objets supplémentaires pour maintenir le comportement. Cela se ferait en introduisant des rôles et des contextes.

DCI == D ata C ollaboration I nteraction

Bien sûr, les données et le comportement du modèle peuvent être implémentés avec des objets javascript simples ou tout modèle de "classe". Mais quelle serait la façon dont AngularJS le ferait? Vous utilisez des services?

Cela revient donc à cette question:

Comment implémentez-vous des modèles découplés du contrôleur, conformément aux meilleures pratiques AngularJS?

Nils Blum-Oeste
la source
12
Je voterais pour cette question si vous pouviez définir l'ICD ou au moins fournir le formulaire épelé. Je n'ai jamais vu cet acronyme dans aucune littérature sur les logiciels. Merci.
Jim Raden
13
Je viens d'ajouter un lien pour DCI comme référence.
Nils Blum-Oeste
1
@JimRaden DCI est Dataq, Context, interaction et est un paradigme formulé tout d'abord par le père de MVC (Trygve Reenskauge). Il y a maintenant pas mal de littérature sur le sujet. Une bonne lecture est Coplien et Bjørnvig "Lean architecture"
Rune FS
3
Merci. Pour le meilleur ou pour le pire, la plupart des gens ne connaissent même pas la littérature originale. Il y a 55 millions d'articles sur MVC, selon Google, mais seulement 250 000 qui mentionnent MCI et MVC. Et sur Microsoft.com? 7. AngularJS.org ne mentionne même pas l'acronyme DCI: "Votre recherche - site: angularjs.org dci - ne correspond à aucun document".
Jim Raden
Les objets ressources sont essentiellement les modèles dans Angular.js .. je les étends.
Salman von Abbas

Réponses:

155

Vous devez utiliser les services si vous voulez quelque chose utilisable par plusieurs contrôleurs. Voici un exemple artificiel simple:

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
la source
23
Quel serait l'avantage d'utiliser un service plutôt que de simplement créer un objet Javascript simple comme modèle et de l'attribuer à la portée du contrôleur?
Nils Blum-Oeste
22
Dans le cas où vous avez besoin de la même logique partagée entre plusieurs contrôleurs. De cette façon, il est plus facile de tester les choses indépendamment.
Andrew Joslin
1
Le dernier exemple en quelque sorte aspiré, celui-ci a plus de sens. Je l'ai édité.
Andrew Joslin
9
Ouais, avec un vieil objet Javascript, vous ne seriez pas en mesure d'injecter quoi que ce soit angulaire dans votre ListService. Comme dans cet exemple, si vous aviez besoin de $ http.get pour récupérer les données de la liste au début, ou si vous aviez besoin d'injecter $ rootScope pour pouvoir diffuser des événements $.
Andrew Joslin
1
Pour rendre cet exemple plus DCI, les données ne devraient-elles pas être en dehors de ListService?
PiTheNumber
81

J'essaie actuellement ce modèle, qui, bien que non DCI, fournit un découplage classique de service / modèle (avec des services pour parler aux services Web (aka modèle CRUD), et un modèle définissant les propriétés et méthodes de l'objet).

Notez que j'utilise uniquement ce modèle chaque fois que l'objet modèle a besoin de méthodes travaillant sur ses propres propriétés, que j'utiliserai probablement partout (comme des getter / setters améliorés). Je ne préconise pas de le faire systématiquement pour chaque service.

EDIT: Je pensais que ce modèle irait à l'encontre du mantra "Le modèle angulaire est un vieil objet javascript", mais il me semble maintenant que ce modèle est parfaitement bien.

EDIT (2): Pour être encore plus clair, j'utilise une classe Model uniquement pour factoriser des getters / setters simples (par exemple: à utiliser dans les modèles de vue). Pour la logique des grandes entreprises, je recommande d'utiliser des services distincts qui "connaissent" le modèle, mais qui en sont séparés et qui n'incluent que la logique métier. Appelez cela une couche de service "expert en affaires" si vous le souhaitez

service / ElementServices.js (remarquez comment Element est injecté dans la déclaration)

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 (en utilisant angularjs Factory, conçu pour la création d'objets)

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
la source
4
J'entre dans Angular, mais je serais curieux de savoir si / pourquoi les vétérans penseraient que c'est de l'hérésie. C'est probablement la façon dont je l'aborderais également au départ. Quelqu'un pourrait-il fournir des commentaires?
Aaronius
2
@Aaronius juste pour être clair: je n'ai jamais réellement lu "vous ne devriez jamais faire ça" sur aucun doc ou blog angularjs, mais j'ai toujours lu des choses comme "angularjs n'a pas besoin d'un modèle, il utilise simplement du vieux javascript" et j'ai dû découvrir ce modèle par moi-même. Comme c'est mon premier vrai projet sur AngularJS, je mets ces avertissements forts, afin que les gens ne copient / collent pas sans y penser d'abord.
Ben G
Je me suis installé sur un modèle à peu près similaire. C'est dommage qu'Angular n'ait pas vraiment de support (ou apparemment envie de supporter) un modèle au sens "classique".
drt
3
Cela ne me semble pas une hérésie, vous utilisez les usines pour lesquelles elles ont été créées: construire des objets. Je pense que l'expression "angularjs n'a pas besoin d'un modèle" signifie "vous n'avez pas besoin d'hériter d'une classe spéciale, ou d'utiliser des méthodes spéciales (comme ko.observable, in knockout) pour travailler avec des modèles en angular, a un objet js pur suffira ".
Felipe Castro
1
Le fait d'avoir un ElementService correctement nommé pour chaque collection n'entraînerait-il pas un tas de fichiers presque identiques?
Collin Allen
29

La documentation d'Angularjs indique clairement:

Contrairement à de nombreux autres frameworks, Angular ne fait aucune restriction ni exigence sur le modèle. Il n'y a pas de classes à hériter ou de méthodes d'accesseur spéciales pour accéder ou modifier le modèle. Le modèle peut être primitif, de hachage d'objet ou un type d'objet complet. En bref, le modèle est un simple objet JavaScript.

- Guide du développeur AngularJS - Concepts V1.5 - Modèle

Cela signifie donc que c'est à vous de déclarer un modèle. C'est un simple objet Javascript.

Personnellement, je n'utiliserai pas les services angulaires car ils étaient censés se comporter comme des objets singleton que vous pouvez utiliser, par exemple, pour conserver des états globaux dans votre application.

SC
la source
Vous devez fournir un lien vers où cela est indiqué dans la documentation. J'ai fait une recherche Google pour "Angular ne fait aucune restriction ni exigence sur le modèle" , et cela ne se retrouve nulle part dans les documents officiels, pour autant que je sache.
4
c'était dans les anciens docs angularjs (celui vivant en répondant): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
SC
8

DCI est un paradigme et en tant que tel, il n'y a pas de méthode angularJS de le faire, que le langage supporte DCI ou non. JS supporte plutôt bien DCI si vous êtes prêt à utiliser la transformation source et avec quelques inconvénients si vous ne l'êtes pas. Encore une fois, DCI n'a pas plus à voir avec l'injection de dépendances que de dire qu'une classe C # a et n'est certainement pas un service non plus. Donc, la meilleure façon de faire DCI avec angulusJS est de faire DCI à la manière JS, ce qui est assez proche de la façon dont DCI est formulé en premier lieu. Sauf si vous effectuez une transformation source, vous ne pourrez pas le faire complètement car les méthodes de rôle feront partie de l'objet même en dehors du contexte, mais c'est généralement le problème avec l'injection de méthode basée sur DCI. Si vous regardez fullOO.infole site faisant autorité pour DCI, vous pouvez jeter un œil aux implémentations ruby, ils utilisent également l'injection de méthode ou vous pouvez jeter un œil ici pour plus d'informations sur DCI. C'est principalement avec des exemples RUby, mais le contenu DCI est agnostique à cela. L'une des clés de DCI est que ce que fait le système est séparé de ce qu'il est. Ainsi, l'objet de données est assez stupide mais une fois lié à un rôle dans un contexte, les méthodes de rôle rendent certains comportements disponibles. Un rôle est simplement un identifiant, rien de plus, lorsque vous accédez à un objet via cet identifiant, des méthodes de rôle sont disponibles. Il n'y a pas d'objet / classe de rôle. Avec l'injection de méthode, la portée des méthodes de rôle n'est pas exactement telle que décrite mais proche. Un exemple de contexte dans JS pourrait être

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
la source
1
Merci d'avoir élaboré le contenu DCI. C'est une excellente lecture. Mais mes questions visent vraiment à "où placer les objets modèles dans angularjs". DCI est juste là pour référence, que je pourrais non seulement avoir un modèle, mais le diviser de la manière DCI. Modifie la question pour la rendre plus claire.
Nils Blum-Oeste
7

Cet article sur les modèles dans AngularJS pourrait aider:

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

marianboda
la source
7
Notez que les réponses uniquement liées aux liens sont déconseillées, les réponses SO devraient être le point final d'une recherche de solution (par rapport à une autre étape de références, qui ont tendance à devenir obsolète au fil du temps). Veuillez envisager d'ajouter un synopsis autonome ici, en conservant le lien comme référence.
kleopatra
l'ajout d'un tel lien dans un commentaire sur la question serait bien.
jorrebor
Ce lien est en fait un très bon article, mais idem, il devrait être transformé en une réponse pour être approprié pour SO
Jeremy Zerr
5

Comme indiqué par d'autres affiches, Angular ne fournit aucune classe de base prête à l'emploi pour la modélisation, mais on peut utilement fournir plusieurs fonctions:

  1. Méthodes d'interaction avec une API RESTful et de création de nouveaux objets
  2. Établir des relations entre les modèles
  3. Valider les données avant de persister dans le backend; également utile pour afficher les erreurs en temps réel
  4. Mise en cache et chargement différé pour éviter de faire des requêtes HTTP inutiles
  5. Etat des crochets de machine (avant / après sauvegarde, mise à jour, création, nouvelle, etc.)

Une bibliothèque qui fait bien toutes ces choses est ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource ). Divulgation complète - j'ai écrit cette bibliothèque - et je l'ai utilisée avec succès dans la création de plusieurs applications à l'échelle de l'entreprise. Il est bien testé et fournit une API qui devrait être familière aux développeurs de Rails.

Mon équipe et moi continuons à développer activement cette bibliothèque, et j'aimerais voir plus de développeurs Angular y contribuer et la tester au combat.

Cassette Brett
la source
Hey! C'est vraiment génial! Je vais le brancher sur mon application dès maintenant. Les tests de combat viennent de commencer.
J.Bruni
1
Je viens de regarder votre message et je me demandais quelles sont les différences entre votre service ngActiveResourceet Angular $resource. Je suis un peu nouveau sur Angular et j'ai parcouru rapidement les deux ensembles de documents, mais ils semblent offrir beaucoup de chevauchements. A été ngActiveResourcedéveloppé avant que le $resourceservice soit disponible?
Eric B.
5

Une question plus ancienne, mais je pense que le sujet est plus pertinent que jamais étant donné la nouvelle direction d'Angular 2.0. Je dirais qu'une meilleure pratique est d'écrire du code avec le moins de dépendances possible sur un framework particulier. Utilisez uniquement les parties spécifiques du cadre où il ajoute une valeur directe.

Actuellement, il semble que le service Angular soit l'un des rares concepts qui parviendront à la prochaine génération d'Angular, il est donc probablement judicieux de suivre la directive générale de déplacement de toute la logique vers les services. Cependant, je dirais que vous pouvez faire des modèles découplés même sans dépendance directe sur les services angulaires. Créer des objets autonomes avec uniquement les dépendances et les responsabilités nécessaires est probablement la voie à suivre. Cela facilite également la vie lors des tests automatisés. La responsabilité unique est un travail de buzz de nos jours, mais cela a beaucoup de sens!

Voici un exemple de bagout que je considère bon pour découpler le modèle objet du dom.

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

Un objectif clé est de structurer votre code de manière à le rendre aussi facile à utiliser à partir de tests unitaires qu'à partir d'une vue. Si vous y parvenez, vous êtes bien placé pour écrire des tests réalistes et utiles.

TGH
la source
4

J'ai essayé de résoudre ce problème exact dans ce billet de blog .

Fondamentalement, le meilleur endroit pour la modélisation de données est dans les services et les usines. Cependant, selon la façon dont vous récupérez vos données et la complexité des comportements dont vous avez besoin, il existe de nombreuses façons différentes de procéder à la mise en œuvre. Angular n'a actuellement ni méthode standard ni meilleure pratique.

Le message couvre trois approches, en utilisant $ http , $ resource et Restangular .

Voici un exemple de code pour chacun, avec une getResult()méthode personnalisée sur le modèle de Job:

Restangulaire (pois facile):

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

$ ressource (un peu plus alambiquée):

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

Le billet de blog lui-même explique plus en détail les raisons pour lesquelles vous pouvez utiliser chaque approche, ainsi que des exemples de code sur la façon d'utiliser les modèles dans vos contrôleurs:

Modèles de données AngularJS: $ http VS $ resource VS Restangular

Il est possible qu'Angular 2.0 offre une solution plus robuste à la modélisation des données qui rassemble tout le monde sur la même page.

Alan Christopher Thomas
la source