javascript - app - Controladores AngularJS, patrón de diseño para un código DRY
ng-if (5)
He creado un ejemplo completo con el propósito de describir este problema. Mi aplicación real es incluso más grande que la demostración presentada y hay más servicios y directivas operados por cada controlador. Esto lleva a más repetición de código. Intenté poner algunos comentarios en el código para aclaraciones, PLUNKER : http://plnkr.co/edit/781Phn?p=preview
Parte repetitiva :
routerApp.controller(''page1Ctrl'', function(pageFactory) {
var vm = this;
// page dependent
vm.name = ''theOne'';
vm.service = ''oneService'';
vm.seriesLabels = [''One1'', ''Two1'', ''Three1''];
// these variables are declared in all pages
// directive variables,
vm.date = {
date: new Date(),
dateOptions: {
formatYear: ''yy'',
startingDay: 1
},
format: ''dd-MMMM-yyyy'',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
})
Básicamente moví toda la lógica que pude a las fábricas y directivas. Pero ahora en cada controlador que usa cierta directiva necesito, por ejemplo, un campo que mantiene el valor que la directiva está modificando. Y su configuración. Más tarde, necesito un campo similar para guardar los datos que provienen del servicio de datos, y la llamada en sí misma (método) también es la misma.
Esto lleva a mucha repetición.
Gráficamente veo el ejemplo actual para verse así:
Si bien creo que el diseño adecuado debería verse más como esto:
Intenté encontrar alguna solución aquí, pero ninguna parece estar confirmada. Lo que he encontrado:
- La estructura del controlador AngularJS DRY sugiere que pase $ scope o vm y lo decore con métodos y campos adicionales. Pero muchas fuentes dicen que es una solución sucia.
- ¿Cuál es la forma recomendada de extender los controladores AngularJS? usando angular.extend, pero esto tiene problemas al usar el
controller as
sintaxis. - Y luego he encontrado también la respuesta (en el enlace de arriba):
No extiendes los controladores. Si realizan las mismas funciones básicas, entonces esas funciones deben trasladarse a un servicio. Ese servicio puede ser inyectado en sus controladores.
E incluso cuando lo hice todavía hay mucha repetición. ¿O es así como tiene que ser? Al igual que John Papa sais ( http://www.johnpapa.net/angular-app-structuring-guidelines/ ):
Trate de mantenerse SECO (No se repita) o T-DRY
¿Te enfrentaste a un problema similar? ¿Cuales son las opciones?
Desde una perspectiva de diseño general, no veo mucha diferencia entre decorar un controlador y extender un controlador. Al final, ambos son una forma de mezcla y no una herencia. Así que realmente se reduce a lo que usted se siente más cómodo trabajando. Una de las grandes decisiones de diseño se reduce no solo a cómo pasar la funcionalidad a todos los controladores, sino también a pasar la funcionalidad a decir 2 de los 3 controladores también.
Decorador de Fábrica
Una forma de hacer esto, como usted menciona, es pasar su $ scope o vm a una fábrica, que decora su controlador con métodos y campos adicionales. No veo esto como una solución sucia, pero puedo entender por qué algunas personas querrían separar las fábricas de su alcance para separar las preocupaciones de su código. Si necesita agregar funcionalidad adicional al escenario 2 de 3, puede pasar fábricas adicionales. Hice un ejemplo plunker de esto .
dataservice.js
routerApp.factory(''pageFactory'', function() {
return {
setup: setup
}
function setup(vm, name, service, seriesLabels) {
// page dependent
vm.name = name;
vm.service = service;
vm.seriesLabels = seriesLabels;
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf(''month'').valueOf(),
dateOptions: {
formatYear: ''yy'',
startingDay: 1
},
format: ''dd-MMMM-yyyy'',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = get(vm.service);
}
//default call
vm.update();
}
});
page1.js
routerApp.controller(''page1Ctrl'', function(pageFactory) {
var vm = this;
pageFactory.setup(vm, ''theOne'', ''oneService'', [''One1'', ''Two1'', ''Three1'']);
})
Controlador de extensión
Otra solución que mencionas es extender un controlador. Esto es factible mediante la creación de un súper controlador que se mezcla con el controlador en uso. Si necesita agregar funcionalidad adicional a un controlador específico, simplemente puede combinar otros súper controladores con funcionalidad específica. Aquí hay un ejemplo de plunker .
ParentPage
routerApp.controller(''parentPageCtrl'', function(vm, pageFactory) {
setup()
function setup() {
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf(''month'').valueOf(),
dateOptions: {
formatYear: ''yy'',
startingDay: 1
},
format: ''dd-MMMM-yyyy'',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
}
})
page1.js
routerApp.controller(''page1Ctrl'', function($controller) {
var vm = this;
// page dependent
vm.name = ''theOne'';
vm.service = ''oneService'';
vm.seriesLabels = [''One1'', ''Two1'', ''Three1''];
angular.extend(this, $controller(''parentPageCtrl'', {vm: vm}));
})
Enrutador UI de estados anidados
Dado que está utilizando ui-router, también puede lograr resultados similares al anidar los estados. Una advertencia a esto es que el $ scope no se pasa del controlador principal al secundario. Entonces, en lugar de eso, debe agregar el código duplicado en $ rootScope. Utilizo esto cuando hay funciones que quiero pasar por todo el programa, como una función para probar si estamos en un teléfono móvil, que no depende de ningún controlador. Aquí hay un ejemplo de plunker .
Me enfrenté completamente a los mismos problemas que describiste. Soy un gran partidario de mantener las cosas en seco. Cuando comencé a usar Angular, no había una forma prescrita o recomendada de hacer esto, así que simplemente reformulé mi código a medida que avanzaba. Como con muchas otras cosas, no creo que sea una forma correcta o incorrecta de hacer estas cosas, así que usa el método con el que te sientas cómodo. Así que debajo está lo que terminé usando y me ha servido bien.
En mis aplicaciones generalmente tengo tres tipos de páginas:
- Página de lista - Lista de tablas de recursos específicos. Puede buscar / filtrar / ordenar sus datos.
- Página de formulario - Crear o editar recurso.
- Página de visualización: página de visualización detallada del recurso / datos.
Descubrí que normalmente hay muchos códigos repetitivos en (1) y (2), y no me refiero a las características que deberían extraerse a un servicio. Entonces, para abordar eso, estoy usando la siguiente jerarquía de herencia:
Listar páginas
- BaseListController
- loadNotification ()
- buscar()
- Búsqueda Avanzada()
- etc ....
- Lista de recursosController
- cualquier cosa específica de recursos
- BaseListController
Páginas de formularios
- Controlador de base
- setServerErrors ()
- clearServerErrors ()
- cosas como advertir al usuario está navegando fuera de esta página antes de guardar el formulario, y cualquier otra característica general.
- ResumenFormController
- salvar()
- processUpdateSuccess ()
- processCreateSuccess ()
- processServerErrors ()
- configurar cualquier otra opción compartida
- Controlador de formulario de recursos
- cualquier cosa específica de recursos
- Controlador de base
Para habilitar esto necesitas algunas convenciones en su lugar. Por lo general, solo tengo una plantilla de vista por recurso para las páginas de formulario. Al usar la funcionalidad de resolve
enrutador, paso una variable para indicar si el formulario se está utilizando para crear o editar, y lo publico en mi vm
. Esto puede ser utilizado dentro de su AbstractFormController
para guardar o actualizar su servicio de datos.
Para implementar la herencia del controlador, uso la función $injector.invoke
Angulars que pasa en this
como la instancia. Ya que $injector.invoke
es parte de la infraestructura de Angulars DI, funciona muy bien ya que manejará cualquier dependencia que necesiten las clases de controlador base, y puedo proporcionar cualquier variable de instancia específica que desee.
Aquí hay un pequeño fragmento de cómo se implementa todo:
Common.BaseFormController = function (dependencies....) {
var self = this;
this.setServerErrors = function () {
};
/* .... */
};
Common.BaseFormController[''$inject''] = [dependencies....];
Common.AbstractFormController = function ($injector, other dependencies....) {
$scope.vm = {};
var vm = $scope.vm;
$injector.invoke(Common.BaseFormController, this, { $scope: $scope, $log: $log, $window: $window, alertService: alertService, any other variables.... });
/* ...... */
}
Common.AbstractFormController[''$inject''] = [''$injector'', other dependencies....];
CustomerFormController = function ($injector, other dependencies....) {
$injector.invoke(Common.AbstractFormController, this, {
$scope: $scope,
$log: $log,
$window: $window,
/* other services and local variable to be injected .... */
});
var vm = $scope.vm;
/* resource specific controller stuff */
}
CustomerFormController[''$inject''] = [''$injector'', other dependencies....];
Para llevar un paso más allá, encontré reducciones masivas en el código repetitivo a través de la implementación de mi servicio de acceso a datos. Para la capa de datos la convención es rey. Descubrí que si mantiene una convención común en la API de su servidor, puede recorrer un largo camino con una fábrica / repositorio / clase base o como quiera que lo llame. La forma en que lo logro en AngularJs es usar una fábrica de AngularJs que devuelve una clase de repositorio base, es decir, la fábrica devuelve una función de clase javascript con definiciones de prototipos y no una instancia de objeto, lo llamo abstractRepository. Luego, para cada recurso, creo un repositorio concreto para ese recurso específico que se hereda de forma prototípica de abstractRepository, por lo que heredo todas las características compartidas / base de abstractRepository y defino cualquier característica específica de recursos en el repositorio concreto.
Creo que un ejemplo será más claro. Supongamos que la API de su servidor utiliza la siguiente convención de URL (no soy el más puro de REST, así que dejaremos la convención para lo que quiera implementar):
GET -> /{resource}?listQueryString // Return resource list
GET -> /{resource}/{id} // Return single resource
GET -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT -> /{resource}/{id} // Update existing resource
POST -> /{resource}/ // Create new resource
etc.
Personalmente uso Restangular, por lo que el siguiente ejemplo se basa en él, pero debería poder adaptarlo fácilmente a $ http o $ resource o la biblioteca que esté usando.
ResumenRepository
app.factory(''abstractRepository'', [function () {
function abstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
abstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + ''view'').get();
},
update: function (updatedResource) {
return updatedResource.put();
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
}
// etc.
};
abstractRepository.extend = function (repository) {
repository.prototype = Object.create(abstractRepository.prototype);
repository.prototype.constructor = repository;
};
return abstractRepository;
}]);
Repositorio concreto, usemos al cliente como ejemplo:
app.factory(''customerRepository'', [''Restangular'', ''abstractRepository'', function (restangular, abstractRepository) {
function customerRepository() {
abstractRepository.call(this, restangular, ''customers'');
}
abstractRepository.extend(customerRepository);
return new customerRepository();
}]);
Así que ahora tenemos métodos comunes para los servicios de datos, que se pueden consumir fácilmente en las clases base del controlador Formulario y Lista.
No puedo responder en el comentario pero aquí lo que haré:
Tendré un ConfigFactory sosteniendo un mapa de variables dependientes de la página:
{
theOne:{
name: ''theOne'',
service: ''oneService'',
seriesLabels: [''One1'', ''Two1'', ''Three1'']
},
...
}
Entonces tendré un LogicFactory con un método newInstance () para obtener un objeto adecuado cada vez que lo necesite. El logicFactory obtendrá todos los datos / métodos compartidos entre los controladores. A este LogicFactory, le daré los datos específicos de la vista. y la vista tendrá que enlazar a esta Fábrica.
Y para recuperar los datos específicos de la vista, pasaré la clave de mi mapa de configuración en el enrutador.
así que digamos que el enrutador le da # current = theOne, lo haré en el controlador:
var specificData = ServiceConfig.get($location.search().current);
this.logic = LogicFactory.newInstance(specificData);
Espero te ayude
Retoco tu ejemplo, aquí está el resultado: http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview
Edición : solo para decir de esta manera, puede cargar la configuración específica desde un servidor remoto que le brinda los datos de vista específica
Para resumir las respuestas anteriores:
Controladores de decoración: como usted dijo, esta es una solución sucia; Imagine tener diferentes fábricas que decoran el mismo controlador, será muy difícil (especialmente para otros desarrolladores) evitar la colisión de propiedades, e igualmente difícil rastrear qué fábrica agregó qué propiedades. En realidad, es como tener una herencia múltiple en la POO, algo que la mayoría de los lenguajes modernos impiden por diseño por las mismas razones.
Uso de una directiva: esta puede ser una gran solución si todos sus controladores van a tener las mismas vistas html, pero aparte de eso, tendrá que incluir una lógica bastante compleja en sus vistas, lo que puede ser difícil de depurar.
El enfoque que propongo es utilizar la composición (en lugar de la herencia con decoradores). Separe toda la lógica repetitiva en las fábricas y deje solo la creación de las fábricas en el controlador.
routerApp.controller(''page1Ctrl'', function (Page, DateConfig, DataService) {
var vm = this;
// page dependent
vm.page = new Page(''theOne'', ''oneService'', [''One1'', ''Two1'', ''Three1'']);
// these variables are declared in all pages
// directive variables,
vm.date = new DateConfig()
// dataservice
vm.dataService = new DataService(vm.page.service);
//default call
vm.dataService.update();
})
.factory(''Page'', function () {
//constructor function
var Page = function (name, service, seriesLabels) {
this.name = name;
this.service = service;
this.seriesLabels = seriesLabels;
};
return Page;
})
.factory(''DateConfig'', function () {
//constructor function
var DateConfig = function () {
this.date = new Date();
this.dateOptions = {
formatYear: ''yy'',
startingDay: 1
};
this.format = ''dd-MMMM-yyyy'';
this.opened = false;
this.open = function ($event) {
this.opened = true;
};
};
return DateConfig;
})
Este código no está probado, pero solo quiero dar una idea. La clave aquí es separar el código en las fábricas y agregarlas como propiedades en el controlador. De esta manera, la implementación no se repite (DRY), y todo es obvio en el código del controlador.
Puede hacer su controlador aún más pequeño envolviendo todas las fábricas en una fábrica más grande (fachada), pero esto puede hacer que estén más estrechamente acoplados.
Puede reducir una gran cantidad de su plantilla utilizando una directiva. He creado uno simple para reemplazar todos sus controladores. Simplemente pase los datos específicos de la página a través de las propiedades, y se enlazarán a su alcance.
routerApp.directive(''pageDir'', function() {
return {
restrict: ''E'',
scope: {},
controller: function(pageFactory) {
vm = this;
vm.date = {
date: moment().startOf(''month'').valueOf(),
dateOptions: {
formatYear: ''yy'',
startingDay: 1
},
format: ''dd-MMMM-yyyy'',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
};
vm.update();
},
controllerAs: ''vm'',
bindToController: {
name: ''@'',
service: ''@'',
seriesLabels: ''=''
},
templateUrl: ''page.html'',
replace: true
}
});
Como puedes ver, no es muy diferente a tus controladores. La diferencia es que para usarlos, usará la directiva en la propiedad de la template
su ruta para inicializarla. Al igual que:
.state(''state1'', {
url: ''/state1'',
template: ''<page-dir '' +
''name="theOne" '' +
''service="oneService" '' +
''series-labels="[/'One1/', /'Two1/', /'Three1/']"'' +
''></page-dir>''
})
Y eso es todo. Yo bifurqué tu Plunk para demostrar. http://plnkr.co/edit/NEqXeD?p=preview
EDITAR: Olvidó agregar que también puede aplicar el estilo a la directiva como desee. Olvidé agregar eso al Plunk cuando estaba eliminando el código redundante.