¿Dónde poner los datos y el comportamiento del modelo? [tl; Dr; Usar servicios]


341

Estoy trabajando con AngularJS para mi último proyecto. En la documentación y tutoriales, todos los datos del modelo se colocan en el alcance 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, es posible que desee reutilizarlo en otro contexto / aplicación. Poner todo en el controlador rompe totalmente el patrón MVC.

Lo mismo es válido para el comportamiento de cualquier modelo. Si usara la arquitectura DCI y el comportamiento separado del modelo de datos, tendría que introducir objetos adicionales para mantener el comportamiento. Esto se haría introduciendo roles y contextos.

DCI == D ata C OLABORACIÓN I nteracciones

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 se trata de esta pregunta:

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


12
Votaría esta pregunta si pudiera definir DCI o al menos proporcionar el formulario detallado. Nunca he visto este acrónimo en ninguna literatura de software. Gracias.
Jim Raden

13
Acabo de agregar un enlace para DCI como referencia.
Nils Blum-Oeste

1
@JimRaden DCI es Dataq, Context, interacción y es un paradigma formulado en primer lugar por el padre de MVC (Trygve Reenskauge). Ya hay bastante literatura sobre el tema. Una buena lectura es Coplien y Bjørnvig "Lean architecture"
Rune FS

3
Gracias. Para bien o para mal, la mayoría de las personas ni siquiera conocen la literatura original por ahora. Según Google, hay 55 millones de artículos sobre MVC, pero solo 250,000 que mencionan MCI y MVC. ¿Y en Microsoft.com? 7. AngularJS.org ni siquiera menciona el acrónimo DCI: "Su búsqueda - sitio: angularjs.org dci - no coincide con ningún documento".
Jim Raden

Los objetos de recursos son básicamente los modelos en Angular.js ... los estoy extendiendo.
Salman von Abbas

Respuestas:


155

Debe usar los servicios si desea algo que puedan utilizar 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
}

23
¿Cuál sería el beneficio de usar un servicio en lugar de simplemente crear un objeto Javascript simple como modelo y asignarlo al alcance del controlador?
Nils Blum-Oeste

22
En caso de que necesite la misma lógica compartida entre múltiples controladores. Además, de esta manera es más fácil probar las cosas de forma independiente.
Andrew Joslin

1
El último ejemplo es una mierda, este tiene más sentido. Lo edité
Andrew Joslin

99
Sí, con un objeto Javascript antiguo y simple no podrías inyectar nada angular en tu ListService. Como en este ejemplo, si necesita $ http.get para recuperar los datos de la Lista al inicio, o si necesita inyectar $ rootScope para poder $ transmitir eventos.
Andrew Joslin

1
Para hacer este ejemplo más DCI, ¿no deberían estar los datos fuera de ListService?
PiTheNumber

81

Actualmente estoy probando este patrón, que, aunque no es DCI, proporciona un desacoplamiento clásico de servicio / modelo (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 por sí mismo propiedades, que probablemente usaré en todas partes (como getter / setters mejorados). Estoy no abogando haciendo esto para cada servicio de forma sistemática.

EDITAR: Solía ​​pensar que este patrón iría en contra del mantra "El modelo angular es un simple objeto javascript antiguo", pero ahora me parece 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 usar en plantillas de vista). Para la lógica de las grandes empresas, recomiendo utilizar servicios separados que "conozcan" sobre el modelo, pero que se mantengan separados de ellos, y solo incluyan la lógica empresarial. Llámelo capa de servicio "experto en negocios" si lo desea

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

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

44
Me estoy metiendo en Angular, pero me gustaría saber si los veteranos pensarían que es una herejía. Esta es probablemente la forma en que inicialmente lo abordaría también. ¿Podría alguien darnos tu opinión?
Aaronius

2
@Aaronius para que quede claro: nunca he leído "nunca deberías hacer eso" en ningún documento o blog de angularjs, pero siempre he leído cosas como "angularjs no necesita un modelo, solo está usando JavaScript antiguo" , y tuve que descubrir este patrón por mi cuenta. Como este es mi primer proyecto real en AngularJS, pongo esas advertencias fuertes, para que la gente no copie / pegue sin pensar primero.
Ben G

Me he decidido por un patrón más o menos similar. Es una pena que Angular no tenga ningún apoyo real (o aparentemente desee apoyar) un modelo en el sentido "clásico".
drt

3
Eso no me parece una herejía, estás usando fábricas para lo que fueron creadas: construir objetos. Creo que la frase "angularjs no necesita un modelo" significa "no necesita heredar de una clase especial, o usar métodos especiales (como ko.observable, en knockout) para trabajar con modelos en angular, un el objeto js puro será suficiente ".
Felipe Castro

1
¿No tener un ElementService con el nombre apropiado para cada colección da como resultado un montón de archivos casi idénticos?
Collin Allen

29

La documentación de Angularjs establece claramente:

A diferencia de muchos otros marcos, Angular no impone restricciones ni requisitos al 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.

- Guía del desarrollador de AngularJS - Conceptos de V1.5 - Modelo

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

Personalmente, no usaré Angular Services, ya que estaban destinados a comportarse como objetos únicos que puede usar, por ejemplo, para mantener estados globales en su aplicación.


Debe proporcionar un enlace a donde se indica esto en la documentación. Hice una búsqueda en Google de "Angular no hace restricciones ni requisitos en el modelo" , y no aparece en ningún lugar de los documentos oficiales, por lo que puedo decir.

44
estaba en los antiguos documentos de angularjs (el que estaba vivo mientras respondía): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
SC

8

DCI es un paradigma y, como tal, no hay una forma angular de hacerlo, ya sea que el lenguaje sea compatible con DCI o no. JS admite DCI bastante bien si está dispuesto a utilizar la transformación de origen y con algunos inconvenientes si no lo está. Una vez más, DCI no tiene más que ver con la inyección de dependencia de lo que dice una clase C # y definitivamente tampoco es un servicio. Entonces, la mejor manera de hacer DCI con angulusJS es hacer DCI a la manera de JS, que está bastante cerca de cómo se formula DCI en primer lugar. A menos que realice la transformación de origen, no podrá hacerlo por completo, ya que los métodos de rol formarán parte del objeto incluso fuera del contexto, pero ese es generalmente el problema con la DCI basada en inyección de métodos. Si miras fullOO.infoEn el sitio autorizado de DCI, puede echar un vistazo a las implementaciones de ruby ​​que también usan el método de inyección o puede echar un vistazo aquí para obtener más información sobre DCI. Es principalmente con ejemplos de RUby, pero las cosas de DCI son agnósticas a eso. Una de las claves de DCI es que lo que hace el sistema está separado de lo que es el sistema. Por lo tanto, los objetos de datos son bastante tontos, pero una vez vinculados a un rol en un contexto, los métodos de rol ponen a disposición cierto comportamiento. 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 un objeto / clase de rol. Con la inyección de métodos, el alcance de los métodos de rol no es exactamente como se describe, sino cercano. Un ejemplo de un 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);
   };
}

1
Gracias por elaborar el material DCI. Es una gran lectura. Pero mis preguntas realmente apuntan a "dónde colocar los objetos del modelo en angularjs". DCI está ahí solo como referencia, que podría no solo tener un modelo, sino dividirlo de la manera DCI. Editará la pregunta para que quede más clara.
Nils Blum-Oeste

7

77
Tenga en cuenta que se desaconsejan las respuestas de solo enlace , las respuestas SO deben ser el punto final de una búsqueda de una solución (frente a otra escala de referencias, que tienden a quedarse obsoletas con el tiempo). Considere agregar una sinopsis independiente aquí, manteniendo el enlace como referencia.
kleopatra

sin embargo, agregar dicho enlace en un comentario sobre una pregunta estaría bien.
jorrebor

Este enlace es en realidad un muy buen artículo, pero lo mismo tendría que ser elaborado en una respuesta para ser apropiado para SO
Jeremy Zerr

5

Como lo indican otros carteles, Angular no proporciona una clase base lista para usar para modelar, pero uno puede proporcionar varias funciones útiles:

  1. Métodos para interactuar con una API RESTful y crear nuevos objetos
  2. Establecer relaciones entre modelos
  3. Validar datos antes de persistir en el back-end; También es útil para mostrar errores en tiempo real
  4. Almacenamiento en caché y carga diferida para evitar solicitudes HTTP innecesarias
  5. Ganchos de máquina de estado (antes / después de guardar, actualizar, crear, nuevo, etc.)

Una biblioteca que hace todas estas cosas bien es ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource ). Divulgación completa: escribí esta biblioteca y la he utilizado con éxito en la creació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 que más desarrolladores de Angular contribuyan a ella y la prueben en batalla.


¡Oye! ¡Esto es realmente genial! Lo conectaré a mi aplicación ahora mismo. Las pruebas de batalla acaban de comenzar.
J. Bruni

1
Yo solo miraba tu publicación y me preguntaba cuáles son las diferencias entre tu servicio ngActiveResourcey el de Angular $resource. Soy un poco nuevo en Angular, y rápidamente examiné ambos conjuntos de documentos, pero parecen ofrecer mucha superposición. ¿Se ngActiveResourcedesarrolló antes de que el $resourceservicio estuviera disponible?
Eric B.

5

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 la menor cantidad posible de dependencias en un marco particular. 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 llegará a la próxima generación de Angular, por lo que probablemente sea inteligente seguir la directriz general de mover toda la lógica a los servicios. Sin embargo, diría que puede hacer modelos desacoplados incluso sin una dependencia directa de los servicios de Angular. Crear objetos autocontenidos 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 automatizadas. La responsabilidad individual es un trabajo zumbido 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 del dom.

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

Un objetivo clave es estructurar su código de una manera que lo haga tan fácil de usar desde una prueba unitaria como desde una vista. Si logra eso, está bien posicionado para escribir pruebas realistas y útiles.


4

He tratado de 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 una mejor práctica.

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

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

Restangular (fácil de guisante):

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 (un poco más complicado):

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 publicación del blog en sí entra en más detalles sobre el razonamiento detrás de por qué podría usar cada enfoque, así como ejemplos de código de cómo usar los modelos en sus controladores:

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

Existe la posibilidad de que Angular 2.0 ofrezca una solución más sólida para el modelado de datos que haga que todos estén en la misma página.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.