web-dev-qa-db-fra.com

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

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. Cela peut être complexe et avoir des attributs privés par exemple. En outre, 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 je voulais utiliser architecture DCI et séparer le comportement du modèle de données, il me faudrait introduire des objets supplémentaires pour contenir le comportement. Cela se ferait en introduisant des rôles et des contextes.

DCI == D ata C ollaboration I interaction

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

Donc, cela revient à cette question:

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

340
Nils Blum-Oeste

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

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
}
154
Andrew Joslin

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

Notez que je n’utilise ce motif que lorsque l’objet modèle a besoin de méthodes seules, que j’utiliserai probablement partout (par exemple, un getter/setters amélioré). Je pas préconise 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 que ce modèle convient parfaitement.

EDIT (2): Pour être encore plus clair, j'utilise une classe Model uniquement pour factoriser des simples accesseurs/régleurs (par exemple: à utiliser dans les modèles de vue). Pour une logique de gestion lourde, je vous recommande d'utiliser des services distincts. qui "connaissent" le modèle, mais en sont séparés et n'incluent que la logique métier. Appelez-le couche de service "expert" 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;
});
80
Ben G

La documentation d'Angularjs indique clairement:

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

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

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

Personnellement, je n'utiliserai pas Angular Services, 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.

29
S.C.

DCI est un paradigme et, en tant que tel, il n’ya pas de façon angularJS de le faire, que le langage prenne en charge DCI ou non. JS supporte plutôt bien DCI si vous souhaitez utiliser la transformation de source et présente des inconvénients si vous ne l'êtes pas. Encore une fois, DCI n’a pas plus à voir avec l’injection de dépendances qu’une classe C # et n’est certainement pas un service non plus. Donc, la meilleure façon de faire DCI avec angulusJS est de faire DCI comme 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 du DCI basé sur l'injection de méthode. Si vous consultez fullOO.info le site faisant autorité pour DCI, vous pouvez consulter les implémentations Ruby qui utilisent également l’injection de méthode ou consulter ici pour plus d’informations sur DCI. C'est principalement avec Ruby exemples, mais le contenu de DCI est agnostique. L'une des clés de DCI est que le système est distinct de ce qu'il est. Donc, les objets de données sont assez stupides, mais une fois liés à un rôle dans un contexte, les méthodes de rôle contextuelles rendent certains comportements disponibles. Un rôle est simplement un identifiant, rien de plus. Lors de l'accès à un objet via cet identifiant, les 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);
   };
}
8
Rune FS
7
marianboda

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

Actuellement, il semble que le service Angular soit l’un des rares concepts pouvant faire l’objet pour la prochaine génération d’Angular. Il est donc probablement judicieux de suivre la directive générale qui consiste à appliquer toute la logique aux services. Cependant, je dirais que vous pouvez créer des modèles découplés même sans dépendance directe avec les services Angular. Créer des objets autonomes avec uniquement les dépendances et les responsabilités nécessaires est probablement la voie à suivre. Cela rend également la vie beaucoup plus facile lors des tests automatisés. La seule responsabilité est un travail passionnant de nos jours, mais cela a beaucoup de sens!

Voici un exemple de motif 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 à ce qu'il soit aussi facile à utiliser à partir d'un test unitaire que d'une vue. Si vous y parvenez, vous êtes bien placé pour écrire des tests réalistes et utiles.

5
TGH

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

  1. Méthodes pour interagir avec une API RESTful et créer de nouveaux objets
  2. Établissement de 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 paresseux pour éviter les requêtes HTTP inutiles
  5. Crochets de la machine d'état (avant/après l'enregistrement, la mise à jour, la création, la création, etc.)

NgActiveResource ( https://github.com/FacultyCreative/ngActiveResource ) est une bibliothèque qui fonctionne bien. Divulgation complète - j'ai écrit cette bibliothèque - et je l'ai utilisée avec succès pour la construction de plusieurs applications d'entreprise. Il est bien testé et fournit une API qui devrait être familière aux développeurs Rails.

Mon équipe et moi-même continuons à développer activement cette bibliothèque, et j'aimerais beaucoup voir plus de développeurs Angular contribuer et la tester à la bataille.

5
Brett Cassette

J'ai essayé de m'attaquer à ce problème dans cet article de blog .

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

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

Voici quelques exemples de code pour chacun, avec une méthode personnalisée getResult() sur le modèle de travail:

Restangular (easy 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 (légèrement plus compliqué):

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

La publication de blog décrit plus en détail le raisonnement qui sous-tend l'utilisation de chaque approche, ainsi que des exemples de code illustrant l'utilisation des modèles dans vos contrôleurs:

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

Il est possible que Angular 2.0 offre une solution plus robuste à la modélisation des données, permettant à tout le monde de se retrouver sur la même page.

4