¿Dónde poner los datos y el comportamiento del modelo?

Estoy trabajando con AngularJS para mi último proyecto. En la documentación y los tutoriales, todos los datos del modelo se incluyen en el scope del controlador. Entiendo que tiene que estar allí para estar disponible para el controlador y, por lo tanto, dentro de las vistas correspondientes.

Sin embargo, no creo que el modelo deba implementarse allí. Puede ser complejo y tener atributos privados, por ejemplo. Además, uno podría querer reutilizarlo en otro contexto / aplicación. Poner todo en el controlador rompe por completo el patrón MVC.

Lo mismo es cierto para el comportamiento de cualquier modelo. Si utilizara la architecture DCI y el comportamiento separado del modelo de datos, tendría que introducir objetos adicionales para mantener el comportamiento. Esto se haría mediante la introducción de roles y contextos.

Por supuesto, los datos y el comportamiento del modelo podrían implementarse con objetos JavaScript simples o cualquier patrón de “clase”. Pero, ¿cuál sería la forma AngularJS de hacerlo? ¿Utilizando servicios?

Entonces todo se reduce a esta pregunta:

¿Cómo implementa los modelos desacoplados del controlador, siguiendo las mejores prácticas de AngularJS?

Debe usar servicios si desea algo utilizable por varios controladores. Aquí hay un simple ejemplo artificial:

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 } 

Actualmente estoy probando este patrón, que, aunque no DCI, proporciona un desacoplamiento de servicio / modelo clásico (con servicios para hablar con servicios web (también conocido como modelo CRUD) y un modelo que define las propiedades y métodos del objeto).

Tenga en cuenta que solo uso este patrón cuando el objeto modelo necesita métodos que funcionen en sus propias propiedades, que probablemente usaré en todas partes (como getter / setters mejorados). No estoy abogando por hacer esto para cada servicio sistemáticamente.

EDITAR: Yo solía pensar que este patrón iría en contra del mantra “El modelo angular es el antiguo objeto javascript simple”, pero me parece ahora que este patrón está perfectamente bien.

EDITAR (2): Para ser aún más claro, utilizo una clase de modelo solo para factorizar getters / setters simples (por ejemplo: para ser utilizado en plantillas de visualización). Para la lógica de las grandes empresas, recomiendo usar servicios independientes que “conozcan” el modelo, pero que se mantengan separados de ellos, y solo incluyan lógica comercial. Llámalo capa de servicio “experto en negocios” si quieres

service / ElementServices.js (observe cómo se inyecta Element en la statement)

 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 (usando angularjs Factory, creado para la creación de objetos)

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

La documentación de Angularjs establece claramente:

A diferencia de muchos otros marcos, Angular no impone restricciones ni requisitos en el modelo. No hay clases para heredar o métodos de acceso especiales para acceder o cambiar el modelo. El modelo puede ser primitivo, hash de objeto o un tipo de objeto completo. En resumen, el modelo es un objeto JavaScript simple.

Entonces, depende de ti cómo declarar un modelo. Es un objeto Javascript simple.

Personalmente, no utilizaré los servicios angulares, ya que deben comportarse como objetos únicos que puede usar, por ejemplo, para mantener estados globales en toda su aplicación.

DCI es un paradigma y, como tal, no existe una forma angular de hacerlo, ya sea el soporte de lenguaje DCI o no. JS admite DCI bastante bien si está dispuesto a utilizar la transformación de fuente y tiene algunos inconvenientes si no lo está. Nuevamente DCI no tiene más que ver con la dependency injection que decir que una clase C # tiene y definitivamente no es un servicio. Entonces, la mejor manera de hacer DCI con angulusJS es hacer DCI de la manera JS, que es muy similar a cómo se formula el DCI en primer lugar. A menos que realice una transformación de origen, no podrá hacerlo completamente, ya que los métodos de rol serán parte del objeto incluso fuera del contexto, pero ese es generalmente el problema con el método basado en inyección DCI. Si mira fullOO.info el sitio autorizado para DCI puede echar un vistazo a las implementaciones de ruby, también usan la inyección de método o puede ver aquí para obtener más información sobre DCI. Es sobre todo con ejemplos de RUby pero las cosas de DCI son agnósticas. Una de las claves para DCI es que lo que hace el sistema está separado de lo que es el sistema. Entonces, los objetos de datos son bastante tontos, pero una vez que están vinculados a un rol en un rol de contexto, los métodos hacen que cierto comportamiento esté disponible. Un rol es simplemente un identificador, nada más, y cuando se accede a un objeto a través de ese identificador, los métodos de rol están disponibles. No hay ningún objeto / clase de rol. Con la inyección de método, el scope de los métodos de función no es exactamente como se describe, pero está cerca. Un ejemplo de contexto en JS podría ser

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

Según lo manifestado por otros carteles, Angular no ofrece una clase base lista para usar para el modelado, pero puede proporcionar varias funciones útiles:

  1. Métodos para interactuar con una API RESTful y crear nuevos objetos
  2. Establecer relaciones entre modelos
  3. Validación de datos antes de persistir en el back-end; también es útil para mostrar errores en tiempo real
  4. Almacenamiento en caché y carga lenta para evitar realizar solicitudes HTTP inútiles
  5. State machine hooks (antes / después de guardar, actualizar, crear, nuevo, etc.)

Una biblioteca que hace todo esto bien es ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource ). Divulgación completa: escribí esta biblioteca, y la utilicé con éxito en la construcción de varias aplicaciones a escala empresarial. Está bien probado y proporciona una API que debería ser familiar para los desarrolladores de Rails.

Mi equipo y yo seguimos desarrollando activamente esta biblioteca, y me encantaría ver a más desarrolladores de Angular contribuir y probarla en batalla.

Una pregunta anterior, pero creo que el tema es más relevante que nunca dada la nueva dirección de Angular 2.0. Yo diría que una mejor práctica es escribir código con tan pocas dependencias en un marco particular como sea posible. Solo use las partes específicas del marco donde agrega valor directo.

Actualmente, parece que el servicio angular es uno de los pocos conceptos que lo harán llegar a la próxima generación de Angular, por lo que probablemente sea inteligente seguir la pauta general de trasladar toda la lógica a los servicios. Sin embargo, yo diría que puede hacer modelos desacoplados incluso sin una dependencia directa de los servicios angulares. Crear objetos autónomos con solo las dependencias y responsabilidades necesarias es probablemente el camino a seguir. También hace la vida mucho más fácil cuando se realizan pruebas automáticas. La única responsabilidad es un trabajo de moda en estos días, ¡pero tiene mucho sentido!

Aquí hay un ejemplo de un patrón que considero bueno para desacoplar el modelo de objeto de la dom.

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

Un objective clave es estructurar su código de una manera que lo haga tan fácil de usar desde una unidad de pruebas como desde una vista. Si lo logra, estará en una buena posición para escribir pruebas realistas y útiles.

Intenté abordar ese problema exacto en esta publicación de blog .

Básicamente, el mejor hogar para el modelado de datos es en servicios y fábricas. Sin embargo, dependiendo de cómo recupere sus datos y la complejidad de los comportamientos que necesita, hay muchas maneras diferentes de llevar a cabo la implementación. Angular actualmente no tiene una forma estándar o la mejor práctica.

La publicación abarca tres enfoques, usando $ http , $ resource y Restangular .

Aquí hay un código de ejemplo para cada uno, con un método getResult() en el modelo de trabajo:

Restangular (fácil campesino):

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

$ recurso (un poco más intrincado):

 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', ['$q', '$http', 'Job', function($q, $http, Job) { return { getAll: function(limit) { var deferred = $q.defer(); $http.get('/api/jobs?limit=' + limit + '&full=true').success(function(data) { var jobs = []; for (var i = 0; i < data.objects.length; i ++) { jobs.push(new Job(data.objects[i])); } deferred.resolve(jobs); }); return deferred.promise; } }; }]) .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 publicación del blog en sí misma entra en más detalles sobre el razonamiento detrás de por qué podría utilizar cada enfoque, así como ejemplos de código de cómo utilizar los modelos en sus controladores:

Modelos de datos AngularJS: $ http VS $ recurso VS Restangular

Existe la posibilidad de que Angular 2.0 ofrezca una solución más robusta para el modelado de datos que atraiga a todos en la misma página.