AngularJS: comprensión del patrón de diseño

147

En el contexto de esta publicación de Igor Minar, líder de AngularJS:

MVC vs MVVM vs MVP . Qué tema tan controvertido que muchos desarrolladores pueden pasar horas y horas debatiendo y discutiendo.

Durante varios años, AngularJS estuvo más cerca de MVC (o más bien una de sus variantes del lado del cliente), pero con el tiempo y gracias a muchas refactorizaciones y mejoras de la API, ahora está más cerca de MVVM : el objeto $ scope podría considerarse el ViewModel que se está utilizando decorado por una función que llamamos un controlador .

Ser capaz de clasificar un marco y ponerlo en uno de los cubos MV * tiene algunas ventajas. Puede ayudar a los desarrolladores a sentirse más cómodos con sus apis al facilitar la creación de un modelo mental que represente la aplicación que se está creando con el marco. También puede ayudar a establecer la terminología utilizada por los desarrolladores.

Una vez dicho esto, prefiero ver a los desarrolladores crear aplicaciones innovadoras que estén bien diseñadas y seguir la separación de preocupaciones, que verlas perder el tiempo discutiendo sobre las tonterías de MV *. Y por esta razón, declaro que AngularJS es el marco MVW: Model-View-Whatever . Donde lo que significa " lo que sea que funcione para usted "

Angular le brinda mucha flexibilidad para separar muy bien la lógica de presentación de la lógica de negocios y el estado de presentación. Úselo para aumentar su productividad y el mantenimiento de la aplicación en lugar de acaloradas discusiones sobre cosas que al final del día no importan tanto.

¿Hay alguna recomendación o guía para implementar el patrón de diseño AngularJS MVW (Model-View-Whatever) en aplicaciones del lado del cliente?

Artem Platonov
fuente
votaron por ... que verlos perder el tiempo discutiendo sobre MV * sin sentido.
Shirgill Farhan
1
No necesita Angular para seguir un patrón de diseño de clase de palabra.
útilBee

Respuestas:

223

Gracias a una gran cantidad de fuentes valiosas, tengo algunas recomendaciones generales para implementar componentes en aplicaciones AngularJS:


Controlador

  • El controlador debe ser solo una capa intermedia entre el modelo y la vista. Intenta hacerlo lo más delgado posible.

  • Se recomienda evitar la lógica de negocios en el controlador. Se debe mover al modelo.

  • El controlador puede comunicarse con otros controladores mediante la invocación de métodos (posible cuando los niños desean comunicarse con los padres) o $ emit , $ broadcast y $ on . Los mensajes emitidos y emitidos deben mantenerse al mínimo.

  • El controlador no debería preocuparse por la presentación o la manipulación DOM.

  • Intenta evitar los controladores anidados . En este caso, el controlador principal se interpreta como modelo. Inyecte modelos como servicios compartidos en su lugar.

  • Ámbito de aplicación en el controlador se debe utilizar para la unión modelo con vista y
    encapsular Ver Modelo como para Presentación modelo patrón de diseño.


Alcance

Trate el alcance como de solo lectura en las plantillas y de solo escritura en los controladores . El propósito del alcance es referirse al modelo, no ser el modelo.

Al hacer un enlace bidireccional (ng-model) asegúrese de no enlazar directamente a las propiedades del alcance.


Modelo

El modelo en AngularJS es un singleton definido por servicio .

El modelo proporciona una forma excelente de separar datos y mostrarlos.

Los modelos son candidatos principales para pruebas unitarias, ya que generalmente tienen exactamente una dependencia (alguna forma de emisor de eventos, en el caso común $ rootScope ) y contienen una lógica de dominio altamente comprobable .

  • El modelo debe considerarse como una implementación de una unidad particular. Se basa en el principio de responsabilidad única. La unidad es una instancia responsable de su propio alcance de lógica relacionada que puede representar una entidad única en el mundo real y describirla en el mundo de la programación en términos de datos y estado .

  • El modelo debe encapsular los datos de su aplicación y proporcionar una API para acceder y manipular esos datos.

  • El modelo debe ser portátil para que pueda transportarse fácilmente a una aplicación similar.

  • Al aislar la lógica de la unidad en su modelo, ha facilitado la localización, actualización y mantenimiento.

  • Model puede usar métodos de modelos globales más generales que son comunes para toda la aplicación.

  • Intente evitar la composición de otros modelos en su modelo utilizando inyección de dependencia si no es realmente dependiente para disminuir el acoplamiento de componentes y aumentar la capacidad de prueba y usabilidad de la unidad .

  • Trate de evitar el uso de oyentes de eventos en modelos. Los hace más difíciles de probar y generalmente mata modelos en términos del principio de responsabilidad única.

Implementación del modelo

Como el modelo debería encapsular cierta lógica en términos de datos y estado, debería restringir arquitectónicamente el acceso a sus miembros para que podamos garantizar un acoplamiento flexible.

La forma de hacerlo en la aplicación AngularJS es definirlo usando el tipo de servicio de fábrica . Esto nos permitirá definir propiedades y métodos privados de manera muy fácil y también devolver los de acceso público en un solo lugar que lo hará realmente legible para el desarrollador.

Un ejemplo :

angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {

  var itemsPerPage = 10,
  currentPage = 1,
  totalPages = 0,
  allLoaded = false,
  searchQuery;

  function init(params) {
    itemsPerPage = params.itemsPerPage || itemsPerPage;
    searchQuery = params.substring || searchQuery;
  }

  function findItems(page, queryParams) {
    searchQuery = queryParams.substring || searchQuery;

    return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
      totalPages = results.totalPages;
      currentPage = results.currentPage;
      allLoaded = totalPages <= currentPage;

      return results.list
    });
  }

  function findNext() {
    return findItems(currentPage + 1);
  }

  function isAllLoaded() {
    return allLoaded;
  }

  // return public model API  
  return {
    /**
     * @param {Object} params
     */
    init: init,

    /**
     * @param {Number} page
     * @param {Object} queryParams
     * @return {Object} promise
     */
    find: findItems,

    /**
     * @return {Boolean}
     */
    allLoaded: isAllLoaded,

    /**
     * @return {Object} promise
     */
    findNext: findNext
  };
});

Crear nuevas instancias

Intente evitar tener una fábrica que devuelva una nueva función capaz, ya que esto comienza a descomponer la inyección de dependencia y la biblioteca se comportará de manera incómoda, especialmente para terceros.

Una mejor manera de lograr lo mismo es usar la fábrica como API para devolver una colección de objetos con métodos getter y setter adjuntos.

angular.module('car')
 .factory( 'carModel', ['carResource', function (carResource) {

  function Car(data) {
    angular.extend(this, data);
  }

  Car.prototype = {
    save: function () {
      // TODO: strip irrelevant fields
      var carData = //...
      return carResource.save(carData);
    }
  };

  function getCarById ( id ) {
    return carResource.getById(id).then(function (data) {
      return new Car(data);
    });
  }

  // the public API
  return {
    // ...
    findById: getCarById
    // ...
  };
});

Modelo global

En general, trate de evitar tales situaciones y diseñe sus modelos correctamente para que pueda inyectarse en el controlador y utilizarse a su vista.

En particular, algunos métodos requieren accesibilidad global dentro de la aplicación. Para hacerlo posible, puede definir la propiedad ' común ' en $ rootScope y vincularla a commonModel durante el arranque de la aplicación:

angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
  $rootScope.common = 'commonModel';
}]);

Todos sus métodos globales vivirán dentro de la propiedad ' común '. Este es algún tipo de espacio de nombres .

Pero no defina ningún método directamente en su $ rootScope . Esto puede conducir a un comportamiento inesperado cuando se usa con la directiva ngModel dentro de su alcance de vista, generalmente ensuciando su alcance y conduce a problemas de anulación de métodos de alcance.


Recurso

Resource le permite interactuar con diferentes fuentes de datos .

Debe implementarse utilizando el principio de responsabilidad única .

En particular, es un proxy reutilizable para puntos finales HTTP / JSON.

Los recursos se inyectan en modelos y brindan la posibilidad de enviar / recuperar datos.

Implementación de recursos

Una fábrica que crea un objeto de recurso que le permite interactuar con fuentes de datos RESTful del lado del servidor.

El objeto de recurso devuelto tiene métodos de acción que proporcionan comportamientos de alto nivel sin la necesidad de interactuar con el servicio $ http de bajo nivel.


Servicios

Tanto el modelo como el recurso son servicios .

Los servicios son unidades de funcionalidad no asociadas y poco acopladas que son independientes.

Los servicios son una característica que Angular ofrece a las aplicaciones web del lado del cliente desde el lado del servidor, donde los servicios se han utilizado comúnmente durante mucho tiempo.

Los servicios en aplicaciones angulares son objetos sustituibles que se conectan entre sí mediante inyección de dependencia.

Angular viene con diferentes tipos de servicios. Cada uno con sus propios casos de uso. Lea los Tipos de servicio de comprensión para obtener más detalles.

Intente considerar los principios principales de la arquitectura de servicio en su aplicación.

En general, según el Glosario de servicios web :

Un servicio es un recurso abstracto que representa la capacidad de realizar tareas que forman una funcionalidad coherente desde el punto de vista de las entidades proveedoras y las entidades solicitantes. Para ser utilizado, un agente proveedor concreto debe realizar un servicio.


Estructura del lado del cliente

En general, el lado del cliente de la aplicación se divide en módulos . Cada módulo debe ser comprobable como una unidad.

Intente definir módulos dependiendo de la característica / funcionalidad o vista , no por tipo. Vea la presentación de Misko para más detalles.

Los componentes del módulo se pueden agrupar convencionalmente por tipos como controladores, modelos, vistas, filtros, directivas, etc.

Pero el módulo en sí sigue siendo reutilizable , transferible y comprobable .

También es mucho más fácil para los desarrolladores encontrar algunas partes del código y todas sus dependencias.

Consulte la Organización del código en grandes aplicaciones AngularJS y JavaScript para más detalles.

Un ejemplo de estructuración de carpetas :

|-- src/
|   |-- app/
|   |   |-- app.js
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- homeCtrl.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html
|   |   |   |-- home.less
|   |   |-- user/
|   |   |   |-- user.js
|   |   |   |-- userCtrl.js
|   |   |   |-- userModel.js
|   |   |   |-- userResource.js
|   |   |   |-- user.spec.js
|   |   |   |-- user.tpl.html
|   |   |   |-- user.less
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- createCtrl.js
|   |   |   |   |-- create.tpl.html
|   |-- common/
|   |   |-- authentication/
|   |   |   |-- authentication.js
|   |   |   |-- authenticationModel.js
|   |   |   |-- authenticationService.js
|   |-- assets/
|   |   |-- images/
|   |   |   |-- logo.png
|   |   |   |-- user/
|   |   |   |   |-- user-icon.png
|   |   |   |   |-- user-default-avatar.png
|   |-- index.html

Buen ejemplo de estructuración angular de aplicaciones es implementado por angular-app - https://github.com/angular-app/angular-app/tree/master/client/src

Esto también es considerado por los generadores de aplicaciones modernas: https://github.com/yeoman/generator-angular/issues/109

Artem Platonov
fuente
55
Tengo una preocupación acerca de: "Es muy recomendable evitar la lógica de negocios en el controlador. Debe moverse al modelo". Sin embargo, de la documentación oficial puede leer: "En general, un controlador no debe intentar hacer demasiado. Debe contener solo la lógica de negocios necesaria para una sola vista". ¿Estamos hablando sobre la misma cosa?
op1ekun
3
Yo diría: trate el controlador como modelo de vista.
Artem Platonov
1
+1. ¡Algunos buenos consejos aquí! 2. Desafortunadamente, el ejemplo de searchModelno sigue los consejos de reutilización. Sería mejor importar las constantes a través del constantservicio. 3. ¿Alguna explicación de lo que se quiere decir aquí ?:Try to avoid having a factory that returns a new able function
Dmitri Zaitsev
1
También sobrescribir la prototypepropiedad del objeto rompe la herencia, en su lugar se puede usarCar.prototype.save = ...
Dmitri Zaitsev
2
@ChristianAichinger, se trata de la naturaleza de la cadena de prototipos de JavaScript que te obliga a usar una objectexpresión de enlace bidireccional para asegurarte de escribir en la propiedad o setterfunción exacta . En caso de usar la propiedad directa de su alcance ( sin un punto ), tiene el riesgo de ocultar la propiedad de destino deseada con la recién creada en el alcance superior más cercano en la cadena de prototipos al escribir en ella. Esto se explica mejor en la presentación de Misko
Artem Platonov
46

Creo que la opinión de Igor sobre esto, como se ve en la cita que ha proporcionado, es solo la punta del iceberg de un problema mucho mayor.

MVC y sus derivados (MVP, PM, MVVM) son buenos y elegantes dentro de un solo agente, pero una arquitectura servidor-cliente es para todos los propósitos un sistema de dos agentes, y las personas a menudo están tan obsesionadas con estos patrones que olvidan que El problema en cuestión es mucho más complejo. Al tratar de adherirse a estos principios, en realidad terminan con una arquitectura defectuosa.

Hagamos esto poco a poco.

Las directrices

Puntos de vista

Dentro del contexto angular, la vista es el DOM. Las pautas son:

Hacer:

  • Variable de alcance actual (solo lectura).
  • Llame al controlador para acciones.

No:

  • Pon cualquier lógica.

Como tentador, breve e inofensivo, esto se ve:

ng-click="collapsed = !collapsed"

Significa prácticamente cualquier desarrollador que ahora, para comprender cómo funciona el sistema, necesitan inspeccionar tanto los archivos Javascript como los HTML.

Controladores

Hacer:

  • Vincula la vista al 'modelo' colocando datos en el alcance.
  • Responder a las acciones del usuario.
  • Tratar con la lógica de presentación.

No:

  • Tratar con cualquier lógica de negocios.

La razón de la última directriz es que los controladores son hermanas de las vistas, no entidades; ni son reutilizables

Se podría argumentar que las directivas son reutilizables, pero las directivas también son hermanas para las vistas (DOM); nunca tuvieron la intención de corresponder a entidades.

Claro, a veces las vistas representan entidades, pero ese es un caso bastante específico.

En otras palabras, los controladores se centrarán en la presentación: si incluye la lógica empresarial, no solo es probable que termine con un controlador inflado y poco manejable, sino que también viola el principio de separación de preocupaciones .

Como tal, los controladores en Angular son realmente más de Presentation Model o MVVM .

Y entonces, si los controladores no deberían tratar con la lógica de negocios, ¿quién debería?

¿Qué es un modelo?

Su modelo de cliente es a menudo parcial y obsoleto

A menos que esté escribiendo una aplicación web fuera de línea, o una aplicación que es terriblemente simple (pocas entidades), es muy probable que su modelo de cliente sea:

  • Parcial
    • O no tiene todas las entidades (como en el caso de la paginación)
    • O no tiene todos los datos (como en el caso de la paginación)
  • Inactivo : si el sistema tiene más de un usuario, en cualquier momento no puede estar seguro de que el modelo que tiene el cliente sea el mismo que el servidor.

El modelo real debe persistir.

En MCV tradicional, el modelo es lo único que se persiste . Siempre que hablemos de modelos, estos deben persistir en algún momento. Su cliente puede manipular modelos a voluntad, pero hasta que el viaje de ida y vuelta al servidor se haya completado con éxito, el trabajo no está hecho.

Consecuencias

Los dos puntos anteriores deberían servir como precaución: el modelo que tiene su cliente solo puede involucrar una lógica comercial parcial, en su mayoría simple.

Como tal, quizás sea aconsejable, dentro del contexto del cliente, usar minúsculas M, por lo que es realmente mVC , mVP y mVVm . Lo grande Mes para el servidor.

Lógica de negocios

Quizás uno de los conceptos más importantes sobre los modelos de negocio es que puede subdividirlos en 2 tipos (omito el tercer punto de vista de negocios, ya que es una historia para otro día):

  • Lógica de dominio : también conocida como reglas empresariales empresariales , la lógica que es independiente de la aplicación. Por ejemplo, dar un modelo con firstNamey sirNamepropiedades, un getter como getFullName()puede considerarse independiente de la aplicación.
  • Lógica de la aplicación : también conocida como reglas comerciales de la aplicación , que es específica de la aplicación. Por ejemplo, verificaciones y manejo de errores.

Es importante enfatizar que ambas cosas dentro del contexto del cliente no son lógicas comerciales 'reales' , solo tratan con la parte que es importante para el cliente. La lógica de la aplicación (no la lógica de dominio) debe tener la responsabilidad de facilitar la comunicación con el servidor y la mayor parte de la interacción del usuario; mientras que la lógica del dominio es en gran medida a pequeña escala, específica de la entidad y basada en la presentación.

La pregunta sigue siendo: ¿dónde los arrojas dentro de una aplicación angular?

Arquitectura de 3 vs 4 capas

Todos estos marcos MVW usan 3 capas:

Tres círculos  Interior - modelo, medio - controlador, exterior - vista

Pero hay dos problemas fundamentales con esto cuando se trata de clientes:

  • El modelo es parcial, rancio y no persiste.
  • No hay lugar para poner la lógica de la aplicación.

Una alternativa a esta estrategia es la estrategia de 4 capas :

4 círculos, de interno a externo: reglas comerciales empresariales, reglas comerciales de aplicaciones, adaptadores de interfaz, marcos y controladores

El verdadero negocio aquí es la capa de reglas de negocio de la aplicación (casos de uso), que a menudo va mal en los clientes.

Esta capa es realizada por interactores (tío Bob), que es más o menos lo que Martin Fowler llama una capa de servicio de script de operación .

Ejemplo concreto

Considere la siguiente aplicación web:

  • La aplicación muestra una lista paginada de usuarios.
  • El usuario hace clic en 'Agregar usuario'.
  • Se abre un modelo con un formulario para completar los detalles del usuario.
  • El usuario llena el formulario y presiona enviar.

Algunas cosas deberían pasar ahora:

  • El formulario debe ser validado por el cliente.
  • Se enviará una solicitud al servidor.
  • Se manejará un error, si hay uno.
  • La lista de usuarios puede o no (debido a la paginación) necesita actualizarse.

¿Dónde tiramos todo esto?

Si su arquitectura involucra un controlador que llama $resource, todo esto sucederá dentro del controlador. Pero hay una mejor estrategia.

Una solución propuesta

El siguiente diagrama muestra cómo se puede resolver el problema anterior al agregar otra capa lógica de aplicación en clientes Angulares:

4 cuadros: DOM apunta al controlador, que apunta a la lógica de la aplicación, que apunta a $ resource

Entonces agregamos una capa entre el controlador a $ resource, esta capa (llamémosla interactor ):

  • Es un servicio . En el caso de los usuarios, se puede llamar UserInteractor.
  • Proporciona métodos correspondientes a casos de uso , encapsulando la lógica de la aplicación .
  • Se controla las peticiones hechas al servidor. En lugar de un controlador que llama a $ resource con parámetros de forma libre, esta capa garantiza que las solicitudes realizadas al servidor devuelvan datos sobre los que puede actuar la lógica de dominio.
  • Decora la estructura de datos devuelta con el prototipo de lógica de dominio .

Y así, con los requisitos del ejemplo concreto anterior:

  • El usuario hace clic en 'Agregar usuario'.
  • El controlador le pide al interactor un modelo de usuario en blanco, está decorado con un método de lógica de negocios, como validate()
  • Al enviarlo, el controlador llama al validate()método modelo .
  • Si falla, el controlador maneja el error.
  • Si tiene éxito, el controlador llama al interactor con createUser()
  • El interactor llama a $ resource
  • Tras la respuesta, el interactor delega cualquier error al controlador, que los maneja.
  • Tras una respuesta exitosa, el interactor se asegura de que, si es necesario, la lista de usuarios se actualice.
Izhaki
fuente
Entonces AngularJS se define MVW (donde W es para lo que sea) ya que puedo elegir tener un Controlador (con toda la lógica de negocios) o un Modelo / Presentador de Vista (sin lógica de negocios pero solo un código para llenar la vista) con BL en un servicio separado? Estoy en lo cierto?
BAD_SEED
La mejor respuesta. ¿Tienes un ejemplo real en GitHub de una aplicación angular de 4 capas?
RPallas
1
@RPallas, no, no (desearía tener tiempo para esto). Actualmente estamos probando una arquitectura donde la 'lógica de la aplicación' es solo un interactor de límites; una resolución entre él y el controlador y un modelo de vista que tiene cierta lógica de vista. Todavía estamos experimentando, así que no el 100% de los pros o contras. Pero una vez hecho, espero escribir un blog en alguna parte.
Izhaki
1
@heringer Básicamente, presentamos modelos: construcciones de OOP que representan entidades de dominio. Son estos modelos los que se comunican con los recursos, no con los controladores. Encapsulan la lógica de dominio. Los controladores llaman modelos, que a su vez llaman recursos.
Izhaki
1
@ alex440 No. Aunque han pasado dos meses desde que una publicación seria sobre este tema está al alcance de mis dedos. Se acerca la Navidad, posiblemente entonces.
Izhaki
5

Un problema menor en comparación con los excelentes consejos en la respuesta de Artem, pero en términos de legibilidad del código, encontré lo mejor para definir la API completamente dentro del returnobjeto, para minimizar el ir y venir en el código para ver las variables de desplazamiento:

angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
  var1: value1,
  var2: value2
  ...
})
.factory('myFactory', function(myConfig) {
  ...preliminary work with myConfig...
  return {
    // comments
    myAPIproperty1: ...,
    ...
    myAPImethod1: function(arg1, ...) {
    ...
    }
  }
});

Si el returnobjeto se ve "demasiado lleno", es una señal de que el Servicio está haciendo demasiado.

Dmitri Zaitsev
fuente
0

AngularJS no implementa MVC de la manera tradicional, sino que implementa algo más cercano a MVVM (Model-View-ViewModel), ViewModel también puede denominarse aglutinante (en caso angular puede ser $ scope). El Modelo -> Como sabemos, el modelo en angular puede ser simplemente objetos JS viejos o los datos en nuestra aplicación

La Vista -> la vista en angularJS es el HTML que ha sido analizado y compilado por angularJS aplicando las directivas o instrucciones o enlaces. El punto principal aquí es angular, la entrada no es solo la cadena HTML simple (innerHTML), sino es DOM creado por el navegador.

ViewModel -> ViewModel es en realidad la carpeta / puente entre su vista y modelo en caso angularJS, es $ scope, para inicializar y aumentar el $ scope que usamos Controller.

Si quiero resumir la respuesta: en la aplicación angularJS $ scope tiene referencia a los datos, el Controlador controla el comportamiento y View maneja el diseño al interactuar con el controlador para comportarse en consecuencia.

Ashutosh
fuente
-1

Para ser claro acerca de la pregunta, Angular utiliza diferentes patrones de diseño que ya encontramos en nuestra programación regular. 1) Cuando registramos nuestros controladores o directivas, fábrica, servicios, etc. con respecto a nuestro módulo. Aquí está ocultando los datos del espacio global. Cuál es el patrón del módulo . 2) Cuando angular usa su verificación sucia para comparar las variables de alcance, aquí usa el Patrón de Observador . 3) Todos los ámbitos padre-hijo en nuestros controladores utilizan el patrón Prototypal. 4) En caso de inyectar los servicios, utiliza Factory Pattern .

En general, utiliza diferentes patrones de diseño conocidos para resolver los problemas.

Naveen Reddy
fuente