javascript - change - AngularJS: entender el patrón de diseño
ng title (5)
AngularJS no implementa MVC de manera tradicional, sino que implementa algo más cercano a MVVM (Model-View-ViewModel), ViewModel también se puede denominar como aglutinante (en caso angular puede ser $ scope). El Modelo -> Como sabemos, el modelo en angular puede ser simplemente un objeto antiguo JS o los datos en nuestra aplicación
La vista -> la vista en angularJS es el HTML que ha sido analizado y compilado por angularJS mediante la aplicación de las directivas o instrucciones o enlaces, el punto principal aquí es en angular la entrada no es simplemente la cadena HTML simple (innerHTML), sino que es DOM creado por el navegador.
ViewModel -> ViewModel es en realidad la carpeta / puente entre su vista y modelo en el caso angularJS es $ scope, para inicializar y aumentar el $ scope usamos Controller.
Si quiero resumir la respuesta: en la aplicación angularJS $ scope tiene referencia a los datos, Controller controla el comportamiento y View maneja el diseño al interactuar con el controlador para comportarse en consecuencia.
En el contexto de este post de Igor Minar, líder de AngularJS:
MVC vs MVVM vs MVP . Qué tema tan controversial 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 API, ahora está más cerca de MVVM : el objeto $ scope podría considerarse el ViewModel que se está decorado por una función que llamamos Controlador .
Ser capaz de categorizar un marco y ponerlo en uno de los contenedores 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.
Habiendo dicho, preferiría que los desarrolladores construyan aplicaciones kick-ass que estén bien diseñadas y sigan la separación de preocupaciones, que verles perder el tiempo discutiendo sobre tonterías de MV *. Y por esta razón, por la presente declaro que AngularJS es un framework MVW - Model-View-Whatever . Donde lo que representa " lo que sea que funcione para ti ".
Angular le brinda mucha flexibilidad para separar bien la lógica de presentación de la lógica comercial y el estado de la presentación. Úselo para alimentar su productividad y mantenimiento de la aplicación en lugar de discusiones acaloradas 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 las aplicaciones del lado del cliente?
Creo que el enfoque de Igor sobre esto, como se ve en la cita que proporcionaste, 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 a todos los efectos 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 cumplir con 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 ámbito actual (solo lectura).
- Llamar al controlador por acciones.
No hacer:
- Pon toda lógica.
Como tentador, breve e inofensivo, esto se ve:
ng-click="collapsed = !collapsed"
Significa casi cualquier desarrollador que ahora entienda cómo funciona el sistema que necesitan para inspeccionar tanto los archivos Javascript como los HTML.
Controladores
Hacer:
- Vincula la vista al ''modelo'' colocando datos en el alcance.
- Responda a las acciones del usuario.
- Tratar con la lógica de presentación.
No hacer:
- Tratar con cualquier lógica de negocios.
El motivo de la última directriz es que los controladores son hermanas de las vistas, no de las entidades; ni son reutilizables.
Se podría argumentar que las directivas son reutilizables, pero las directivas también son hermanas para los puntos de vista (DOM): nunca se pensó que correspondieran a entidades.
Claro, a veces las vistas representan entidades, pero ese es un caso bastante específico.
En otras palabras, los controladores se concentrarán en la presentación: si aplica la lógica comercial, no solo es probable que termine con un controlador inflado y poco manejable, sino que también viola el principio de separación de la preocupación .
Como tal, los controladores en Angular son realmente más de Presentation Model o MVVM .
Entonces, si los controladores no deben lidiar con la lógica de negocios, ¿quién debería?
¿Qué es un modelo?
Su modelo de cliente a menudo es parcial y obsoleto
A menos que esté escribiendo una aplicación web fuera de línea, o una aplicación que sea terriblemente simple (pocas entidades), es muy probable que su modelo de cliente sea:
- Parcial
- O bien 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)
- Atrasado : 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 que tiene el servidor.
El modelo real debe persistir
En MCV tradicional, el modelo es lo único que persiste . Cuando hablamos 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á terminado.
Consecuencias
Los dos puntos anteriores deben servir como precaución: el modelo que su cliente tiene solo puede implicar una lógica de negocios parcial, en su mayoría simple.
Como tal, tal vez sea prudente, dentro del contexto del cliente, usar M
minúscula, por lo que es realmente mVC , mVP y mVVm . La gran M
es para el servidor.
Lógica de negocios
Tal vez uno de los conceptos más importantes sobre los modelos de negocios es que puedes subdividirlos en 2 tipos (omito el tercero de vista empresarial 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, proporcione un modelo con las
firstName
ysirName
, un getter comogetFullName()
puede considerarse independiente de la aplicación. - Lógica de aplicación , también conocida como reglas comerciales de aplicación , que es específica de la aplicación. Por ejemplo, verificaciones y manejo de errores.
Es importante enfatizar que ambos en un contexto de cliente no son una lógica comercial "real" : solo tratan con la parte que es importante para el cliente. La lógica de aplicación (no la lógica de dominio) debería 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 parte de pequeña escala, específica de la entidad y orientada a la presentación.
La pregunta aún permanece: ¿dónde los arrojas dentro de una aplicación angular?
Arquitectura de 3 vs 4 capas
Todos estos marcos MVW usan 3 capas:
Pero hay dos cuestiones fundamentales con esto cuando se trata de clientes:
- El modelo es parcial, obsoleto 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 :
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 se realiza por interactores (Uncle 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 debe manejar un error, si hay uno.
- La lista de usuarios puede o no (debido a la paginación) necesita actualización.
¿Dónde arrojamos 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 de lógica de aplicación en clientes angulares:
Así que agregamos una capa entre el controlador a $ recurso, esta capa (vamos a llamarlo 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 .
- Controla las solicitudes hechas al servidor. En lugar de un controlador que llame $ resource con parámetros de forma libre, esta capa asegura que las solicitudes realizadas al servidor devuelvan datos sobre qué lógica de dominio puede actuar.
- 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 solicita al interactor un modelo de usuario en blanco, el que está decorado con el método de lógica de negocios, como
validate()
- Una vez presentado, el controlador llama al método
validate()
del modelo. - Si falla, el controlador maneja el error.
- Si tiene éxito, el controlador llama al interactor con
createUser()
- El interactor llama $ recurso
- Tras la respuesta, el interactor delega cualquier error al controlador, que los maneja.
- Tras una respuesta exitosa, el interactor asegura que, si es necesario, la lista de usuarios se actualiza.
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 encarecidamente evitar la lógica comercial en el controlador. Debería moverse a modelar.
El controlador puede comunicarse con otros controladores utilizando la invocación del método (posible cuando los niños quieren comunicarse con los padres) o los métodos $ emitir , $ emitir y $ on . Los mensajes emitidos y transmitidos 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 padre se interpreta como modelo. Inyecte modelos como servicios compartidos en su lugar.
El alcance en el controlador se debe usar para vincular el modelo con vista y
Encapsulando el Modelo de Vista como para el patrón de diseño del Modelo de Presentación .
Alcance
Trate el ámbito como de solo lectura en plantillas y solo de escritura en controladores . El objetivo del alcance es hacer referencia al modelo, no a ser el modelo.
Al hacer una unión bidireccional (ng-model), asegúrese de no vincularse directamente con las propiedades del alcance.
Modelo
El modelo en AngularJS es un singleton definido por el servicio .
El modelo proporciona una excelente manera de separar los datos y la visualización.
Los modelos son candidatos principales para las pruebas unitarias, ya que normalmente 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 sola entidad 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 esa información.
El modelo debe ser portátil para que pueda ser transportado fácilmente a una aplicación similar.
Al aislar la lógica de la unidad en su modelo, ha facilitado su localización, actualización y mantenimiento.
El modelo puede usar métodos de modelos globales más generales que son comunes para toda la aplicación.
Trate de evitar la composición de otros modelos en su modelo usando la inyección de dependencia si no es realmente dependiente para disminuir el acoplamiento de los componentes y aumentar la capacidad de prueba y usabilidad de la unidad.
Intenta evitar el uso de oyentes de eventos en los modelos. Los hace más difíciles de evaluar y generalmente mata a los modelos en términos de principio de responsabilidad única.
Implementación del modelo
Como el modelo debe encapsular cierta lógica en términos de datos y estado, debe restringir arquitectónicamente el acceso a sus miembros, por lo que podemos 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 muy fácilmente y también devolver los accesibles públicamente en un solo lugar que lo harán 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
};
});
Creando 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 torpe, especialmente para terceros.
Una mejor manera de lograr lo mismo es utilizar la fábrica como una API para devolver una colección de objetos con los 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, intente evitar tales situaciones y diseñe sus modelos correctamente, de modo que pueda inyectarse en el controlador y utilizarse en su vista.
En caso 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 un tipo de espacio de nombres .
Pero no defina ningún método directamente en su $ rootScope . Esto puede llevar a un comportamiento inesperado cuando se usa con la directiva ngModel dentro de su alcance de vista, generalmente dejando a un lado su alcance y conduce a métodos de alcance que anulan los problemas.
Recurso
El recurso le permite interactuar con diferentes fuentes de datos .
Debería implementarse utilizando el principio de responsabilidad única .
En el caso particular, es un proxy reutilizable para los puntos finales HTTP / JSON.
Los recursos se inyectan en los modelos y brindan la posibilidad de enviar / recuperar datos.
Implementación de recursos
Una fábrica que crea un objeto de recursos 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 autónomas.
Los servicios son una característica que Angular brinda 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 mediante la inyección de dependencia.
Angular viene con diferentes tipos de servicios. Cada uno con sus propios casos de uso. Lea Entender los tipos de servicio para más detalles.
Intente considerar los principios principales de la arquitectura del servicio en su aplicación.
En general de acuerdo con 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 entidades de proveedores y entidades de solicitantes. Para ser utilizado, un agente proveedor 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 poderse probar como una unidad.
Intente definir módulos en función de la función / funcionalidad o vista , no por tipo. Ver la presentación de Misko para más detalles.
Los componentes del módulo se pueden agrupar de manera convencional por tipos como controladores, modelos, vistas, filtros, directivas, etc.
Pero el módulo en sí mismo 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 Code Organization en Large AngularJS y JavaScript Applications para obtener más información.
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 lo consideran los generadores de aplicaciones modernos - https://github.com/yeoman/generator-angular/issues/109
Para ser claro acerca de la pregunta, Angular usa diferentes patrones de diseño que ya hemos encontrado 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 utiliza su comprobación sucia para comparar las variables de ámbito, aquí utiliza el patrón de observador . 3) Todos los ámbitos hijos primarios en nuestros controladores usan 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.
Un problema menor comparado con los grandes consejos en la respuesta de Artem, pero en términos de legibilidad del código, me pareció mejor definir la API completamente dentro del objeto de return
, para minimizar el ir y venir en el código para ver qué variables están definidas:
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 objeto de return
se ve como "demasiado concurrido", es una señal de que el Servicio está haciendo demasiado.