AngularJS: ¿Cómo deberían estructurarse los controladores y las fábricas/servicios con un modelo de objetos rico y jerárquico?


Leí estos dos grandes artículos: {[12]]}

El estado de los controladores angularjs por Jonathan Creamer

Y

Repensando los controladores AngularJS por Todd Motto

En estos artículos, los autores hablan de la forma correcta de usar los controladores (haciéndolos puentes anémicos entre la vista y el modelo) y las fábricas/servicios (donde realmente debería vivir la lógica de negocios).

Esta es una gran información, y estaba muy emocionado de comience a refactorizar los controladores en uno de mis proyectos, pero rápidamente descubrí que la estructura que se muestra en los artículos se descompone si tiene un modelo de objetos enriquecido.

Aquí hay un resumen de la configuración de "Rethinking Angularjs Controllers":

Aquí está el controlador:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {

    var vm = this;

    vm.messages = InboxFactory.messages;

    vm.openMessage = function (message) {
      InboxFactory.openMessage(message);
    };

    vm.deleteMessage = function (message) {
      InboxFactory.deleteMessage(message);
    };

    InboxFactory
      .getMessages()
      .then(function () {
      vm.messages = InboxFactory.messages;
    });

});

Y aquí está la fábrica:

app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {

  factory.messages = [];

  factory.openMessage = function (message) {
    $location.search('id', message.id).path('/message');
  };

  factory.deleteMessage = function (message) {
    $http.post('/message/delete', message)
    .success(function (data) {
      factory.messages.splice(index, 1);
      NotificationFactory.showSuccess();
    })
    .error(function () {
      NotificationFactory.showError();
    });
  };

  factory.getMessages = function () {
    return $http.get('/messages')
    .success(function (data) {
      factory.messages = data;
    })
    .error(function () {
      NotificationFactory.showError();
    });
  };

  return factory;

});

Esto es genial y debido a que providers (la fábrica) son singletons, los datos se mantienen en todas las vistas y se puede acceder sin tener que recargarlos desde la API.

Esto funciona bien si messages son un objeto de nivel superior. ¿Pero qué pasa si no lo son? ¿Qué pasa si esta es una aplicación para navegar por las bandejas de entrada de otros usuarios? Tal vez eres un administrador y quieres ser capaz de administrar y navegar por las bandejas de entrada de cualquier usuario. Tal vez necesite múltiples bandejas de entrada de usuarios cargadas al mismo tiempo. ¿Cómo funciona esto? El problema es que los mensajes de la bandeja de entrada se almacenan en el servicio, es decir, InboxFactory.messages.

¿Qué pasa si la jerarquía es como esto:

                           Organization
                                |
              __________________|____________________
             |                  |                    |
         Accounting       Human Resources            IT
             |                  |                    |
     ________|_______      _____|______        ______|________
    |        |       |    |     |      |      |      |        |
   John     Mike    Sue  Tom   Joe    Brad   May    Judy     Jill
    |        |       |    |     |       |     |      |        |
   Inbox    Inbox  Inbox Inbox Inbox  Inbox Inbox  Inbox    Inbox

Ahora messages hay varios niveles profundos en la jerarquía, y no tienen significado por sí mismos. No puede almacenar mensajes en la fábrica, InboxFactory.messages porque tiene que recuperar mensajes de varios usuarios a la vez.

Ahora tendrá una OrganizationFactory, una DepartmentFactory, una UserFactory y una InboxFactory. Recuperar "mensajes" debe estar en el contexto de un user, que está en el contexto de un department, que está en el contexto de un organization. Cómo y dónde deben los datos ser almacenado? ¿Cómo debe ser retirada?

Entonces, ¿cómo se debe resolver esto? ¿Cómo deben estructurarse los controladores, las fábricas/servicios y los modelos de objetos enriquecidos?

En este punto de mi pensamiento, me inclino hacia simplemente mantenerlo delgado y no tener un modelo de objetos ricos. Simplemente almacene los objetos en el scope scope inyectado en el controlador, y si navega a una nueva vista, vuelva a cargar desde la API. Si necesita algunos datos persistieron en las vistas, puede construir ese puente con un servicio o fábrica, pero no debería ser la forma en que haces la mayoría de las cosas.

¿Cómo han resuelto esto otros? ¿Hay algún patrón ahí fuera para esto?

Author: John Slegers, 2015-04-16

3 answers

Puede usar un modelo de objetos enriquecido, pero para los objetos que no son de nivel superior, sus fábricas deben exponer una api para crear nuevas instancias en lugar de usarse como singletons. Esto es algo contrario al diseño de muchas aplicaciones que ves en estos días, que son más funcionales que orientadas a objetos not No estoy comentando los pros y los contras de ninguno de los enfoques, y no creo que Angular te obligue a adoptar uno u otro.

Su ejemplo, rediseñado, en pseudocódigo:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {

   var inbox = InboxFactory.createInbox();

   $scope.getMessages = function(){
      inbox.getMessages()
           .then(...)

   $scope.deleteMessages = function(){
      inbox.deleteMessages()
           .then(...)

});
 3
Author: AlexMA,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-16 20:10:12

Su situación se vuelve mucho más simple si adopta un enfoque basado en rutas (a la ngRoute o algo similar). Considere esta alternativa-advertencia código no probado:

app.config(function($routeProvider) {
  $routeProvider
    .when('/inbox/:inboxId',
      templateUrl: 'views/inbox.html',
      controller: 'InboxCtrl',
      controllerAs: 'inbox',
      resolve: {
        inboxMessages: function(InboxFactory) {
          // Use use :inboxId route param if you need to work with multiple
          // inboxes. Taking some libery here, we'll assuming
          // `InboxFactory.getMessages()` returns a promise that will resolve to
          // an array of messages;
          return InboxFactory.getMessages();
        }
      }
    // ... other routes
    .otherwise: {
      // ...
    };
});

app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) {
  var vm = this;
  vm.messages = inboxMessages;
  vm.openMessage = InboxFactory.openMessage;
  vm.deleteMessage = InboxFactory.deleteMessage;
});

¡Mira lo delgado que es el controlador ahora! Por supuesto que hice uso de una sintaxis más compacta en un par de puntos, pero esto destaca cómo nuestro controlador realmente está pegando las cosas.

Podemos simplificar aún más las cosas al deshacernos de InboxFactory.messages, ¿cuándo lo usaríamos realmente? Sólo estamos garantizados para tener que que se rellene después de InboxFactory.getMessages resuelve, así que vamos a tener esta promesa resolver a los propios mensajes.

Almacenar datos en singletons de esta manera puede ser la solución más fácil en algunos casos, pero hace la vida difícil cuando esos datos deben ser recuperados sobre la marcha. Lo mejor será apoyarse en las API y las fábricas( como sugiere AlexMA), extraer los datos necesarios cada vez que cambie una ruta (por ejemplo, el usuario quiere ver una bandeja de entrada diferente) e inyectar esos datos directamente en el controlador apropiado.

Otro beneficio de este formulario es que tenemos nuestros datos a mano en el momento en que el controlador es instanciado. No tenemos que hacer malabares con estados asincrónicos o preocuparnos por poner mucho código en las devoluciones de llamada. Como corolario, podemos detectar errores de carga de datos antes de mostrar una nueva vista de bandeja de entrada y el usuario no se queda atascado en un estado medio horneado.

Además del punto de su pregunta, sin embargo, tenga en cuenta que la carga de saber cómo su modelo rico la estructura encaja entre sí ya no es problema del controlador. Solo obtiene algunos datos y expone un montón de métodos a la vista.

 2
Author: jtrussell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-26 18:49:14

Después de MUCHO retoque y probar diferentes enfoques, mi decisión final es que no debe persistir en su modelo de objetos enriquecidos a través de las vistas.

Mantengo el modelo de objeto súper delgado y cargo justo lo que necesito para cada vista. Hay datos de alto nivel que guardo alrededor(información del usuario como nombre, id, correo electrónico, etc. y los datos de la organización, como con qué organización están conectados), pero todo lo demás se carga para la vista actual.

Con este enfoque lean, esto es lo que mi la fábrica se vería así:

app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {

factory.messages = [];

factory.openMessage = function (message) {
  $location.search('id', message.id).path('/message');
};

factory.deleteMessage = function (message) {
  $http.post('/message/delete', message)
  .success(function (data) {
    NotificationFactory.showSuccess();
    return data;
  })
  .error(function () {
    NotificationFactory.showError();
    return null;
  });
};

factory.getMessages = function (userId) {
  return $http.get('/messages/user/id/'+userId)
  .success(function (data) {
    return data;
  })
  .error(function () {
    NotificationFactory.showError();
    return null;
  });
};

return factory;

});

Y el controlador:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {

  var vm = this;

  vm.messages = {};

  vm.openMessage = function (message) {
    InboxFactory.openMessage(message);
  };

  vm.deleteMessage = function (message) {
    InboxFactory.deleteMessage(message);
  };

  InboxFactory
    .getMessages(userId) //you can get the userId from anywhere you want.
    .then(function (data) {
      vm.messages = data;
  });

});

Los beneficios hasta ahora son:

  • lógica de aplicación simplificada
  • delgado y medio, ligero (solo cargo lo que necesito para el estado actual)
  • menos uso de memoria, lo que se traduce en un mejor rendimiento general, especialmente en dispositivos móviles
 0
Author: richard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-24 00:44:54