Angular: dependencia circular del caso específico

Hace algún time, comencé a refactorizar mi código del proyecto principal, desacoplando la lógica comercial de los controlleres a los services, de acuerdo con las directrices. Todo fue bien, hasta que enfrenté el problema de la dependencia circular (CD). Leí algunos resources sobre este problema:

Pregunta 1 sobre desbordamiento de stack

Pregunta 2 sobre desbordamiento de stack

Blog Miško Hevery

Desafortunadamente, para mí no está claro ahora, ¿cómo puedo resolver el problema del CD para mi proyecto? Por lo tanto, preparé una pequeña demostración, que representa las características principales de mi proyecto:

Enlace a Plunker

Enlace a GitHub

Breve descripción de los componentes:

  • gridCtrl no contiene ninguna lógica comercial, solo desencadena la carga de datos y cuando los datos están listos, muestra la grilla.
  • gridMainService Este es el service principal, que contiene object gridOptions . Este object es un object central de la cuadrícula, contiene algunas API, inicializadas por marco, datos de filas, encabezados de columnas, etc. A partir de este service, estaba planeando controlar todo lo relacionado con la networking. La function loadGridOptions es activada por gridCtrl y espera hasta que los services correspondientes carguen los datos de fila y las definiciones de columna. Luego, se inicializa con este object gridOption de datos.
  • gridConfigService Este es un service simple, que solía cargar definiciones de columna. No hay problema aqui
  • gridDataService Este service solía cargar datos de fila con la function loadRowData . Otra característica de este service: simula las actualizaciones en vivo, provenientes del server (function $ interval). ¡Problema 1 aquí!
  • gridSettingsService Este service se usa para aplicar algunas configuraciones a la grilla (por ejemplo, cómo deben orderarse las columnas). ¡Problema 2 aquí!

Problema 1: Tengo una dependencia circular entre gridMainService y gridDataService . Primero gridMainService utiliza gridDataService para cargar datos de fila con la function:

self.loadRowData = function () { // implementation here } 

Pero entonces, gridDataService recibe las actualizaciones del server y tiene que insert algunas filas en la grilla. Entonces, tiene que usar gridMainService :

 $interval(function () { var rowToAdd = { make: "VW " + index, model: "Golf " + index, price: 10000 * index }; var newItems = [rowToAdd]; // here I need to get access to the gridMainService var gridMainService = $injector.get('gridMainService'); gridMainService.gridOptions.api.addItems(newItems); index++; }, 500, 10); 

Entonces, aquí me enfrenté al primer CD.

Problema 2: Tengo una dependencia circular entre gridMainService y gridSettingsService . Primero, la activación de gridMainService , cuando la grilla se carga inicialmente y luego configura las configuraciones pnetworkingeterminadas con la siguiente function:

 self.onGridReady = function () { $log.info("Grid is ready, apply default settings"); gridSettingsService.applyDefaults(); }; 

Pero, para realizar algunos cambios, gridSettingsService necesita un acceso a gridMainService y su object gridOptions para aplicar configuraciones:

  self.applyDefaults = function() { var sort = [ {colId: 'make', sort: 'asc'} ]; var gridMainService = $injector.get('gridMainService'); gridMainService.gridOptions.api.setSortModel(sort); }; 

Pregunta: ¿Cómo puedo resolver estos casos de CD de manera adecuada? Porque Miško Hevery Blog fue bastante corto y bueno, pero no he logrado aplicar su estrategia a mi caso.

Y actualmente, no me gusta mucho un enfoque de inyección manual, porque tendré que usarlo mucho y el código se ve un poco borroso.

Tenga en count: preparé solo una demostración de mi gran proyecto. Probablemente puedas aconsejarte poner todo el código en gridDataService y listo. Pero, ya tengo 500 LOC en este service, si fusiono todos los services, será una pesadilla de ~ 1300 LOC.

En este tipo de problemas, hay muchas soluciones, que dependen de la forma en que piense. Prefiero pensar que cada service (o class) tiene algún objective y necesita algún otro service para completar su objective, pero su objective es uno, claro y pequeño. Veamos tu código por esta vista.

Problema 1:

GridData : Aquí viven los datos de la grilla. MainService viene aquí para get los datos que necesita, así que inyectamos esto a mainService y usamos la function loadRowData para get los datos de rowData como lo hace, pero en $ interval inyecta mainService dentro de gridData pero gridData no necesita mainService para finalizar su objective (get elementos del server).

Resuelvo este problema usando un patrón de layout de observador (usando $ rootScope). Eso significa que recibo una notificación cuando se reciben los datos y el service principal viene y los obtiene.

grid-data.service.js :

 angular.module("gridApp").service("gridDataService", ["$injector", "$interval", "$timeout", "$rootScope", function ($injector, $interval, $timeout, $rootScope) { […] $interval(function () { [..] self.newItems = [rowToAdd]; // delete this code // var gridMainService = $injector.get('gridMainService'); // gridMainService.gridOptions.api.addItems(newItems); // notify the mainService that new data has come! $rootScope.$broadcast('newGridItemAvailable'); 

grid-main.service.js :

  angular.module("gridApp").service("gridMainService", ["$log", "$q", "gridConfigService", "gridDataService", '$rootScope', function ($log, $q, gridConfigService, gridDataService, $rootScope) { [..] // new GridData data arrive go to GridData to get it! $rootScope.$on('newGridItemAvailable', function(){ self.gridOptions.api.addItems(gridDataService.getNewItems()); }) [..] 

Cuando se utiliza un server real, la solución más común es usar las promises (no el patrón del observador), como loadRowData.

Problema 2 :

gridSettingsService : Este service cambia la configuration de mainService por lo que necesita mainService pero mainService no se preocupa por gridSettings, cuando alguien quiere cambiar o aprender mainService, el estado interno (data, form) debe comunicarse con la interfaz mainService.

Por lo tanto, elimino la inyección de configuration de grilla de gridMainService y solo brindo una interfaz para poner una function de callback cuando Grid esté listo.

grid-main.service.js :

 angular.module("gridApp").service("gridMainService", ["$log", "$q", "gridConfigService", "gridDataService", '$rootScope', function ($log, $q, gridConfigService, gridDataService, $rootScope) { […] // You want a function to run onGridReady, put it here! self.loadGridOptions = function (onGridReady) { [..] self.gridOptions = { columnDefs: gridConfigService.columnDefs, rowData: gridDataService.rowData, enableSorting: true, onGridReady: onGridReady // put callback here }; return self.gridOptions; }); [..]// I delete the onGridReady function , no place for outsiders // If you want to change my state do it with the my interface 

Ag-grid-controller.js :

  gridMainService.loadGridOptions(gridSettingsService.applyDefaults).then(function () { vm.gridOptions = gridMainService.gridOptions; vm.showGrid = true; }); 

aquí el código completo: https://plnkr.co/edit/VRVANCXiyY8FjSfKzPna?p=preview

Puede presentar un service por separado que expone llamadas específicas, como agregar elementos a la cuadrícula. Ambos services tendrán una dependencia de este service de API, que permite que el service de datos deje de depender del service principal. Este service por separado requerirá que su service principal registre una callback que debería usarse cuando desee agregar un artículo. El service de datos a su vez podrá hacer uso de esta callback.

 angular.module("gridApp").service("gridApiService", function () { var self = this; var addItemCallbacks = []; self.insertAddItemCallback = function (callback) { addItemCallbacks.push(callback); }; self.execAddItemCallback = function (item) { addItemCallbacks.forEach(function(callback) { callback(item); }); }; }); 

En el ejemplo anterior, hay dos funciones disponibles del service. La function de inserción le permitirá registrar una callback desde su function principal, que puede usarse en otro momento. La function ejecutiva permitirá que su service de datos haga uso de la callback almacenada, pasando el nuevo elemento.

En primer lugar, estoy de acuerdo con los otros comentarios de que el desacoplamiento parece ser un poco extremo; no obstante, estoy seguro de que usted sabe más que nosotros cuánto se necesita para su proyecto.

Creo que una solución aceptable es utilizar el pub / sub (patrón de observador), en el que los services gridSettingsService y gridDataService no actualizan directamente el gridMainService , sino que gridMainService una notificación que gridMainService puede enganchar y usar para actualizarse.

Entonces, usando el plunker que me diste, los cambios que haría serían:

  1. Inyecte el service $rootScope en gridSettingsService , gridDataService y gridMainService .

  2. Desde gridSettingsService y gridDataService , detenga la inyección manual de gridMainService y, en su lugar, solo gridMainService $broadcast la notificación con los datos asociados:

“ `

 // in gridDataService $rootScope.$broadcast('GridDataItemsReady', newItems); // in gridSettingsService $rootScope.$broadcast('SortModelChanged', sort); 

“ `

  1. En gridMainService , enumerado a las notifications y actualizar utilizando los datos pasados:

“ `

 // in gridMainService $rootScope.$on('GridDataItemsReady', function(event, newItem) { self.gridOptions.api.addItems(newItem); }); $rootScope.$on('SortModelChanged', function(event, newSort) { self.gridOptions.api.setSortModel(newSort); }); 

“ `

Y aquí está el plunker actualizado: https://plnkr.co/edit/pl8NBxU5gdqU8SupnMgy