Angular: case-specific circular dependency

Some time ago, I began to reorganize my code for the main project, separating business logic from controllers to services, in accordance with the recommendations. Everything went well until I ran into the problem of circular dependency (CD). I read some resources about this issue:

Question 1 about stack overflow

Question 2 about stack overflow

Miško Hevery Blog

Unfortunately, for me it is not clear how I can solve the problem with the CD for my project. Therefore, I prepared a small demo that presents the main features of my project:

Link to Plunker

GitHub Link

A brief description of the components:

  • gridCtrl Does not contain any business logic, only starts the dataload and when the data is ready, show grid.
  • gridMainService . This is the main service that contains gridOptions objects. This object is the main object of the grid, contains some api initialized by the wireframe, row data, column headings, etc. From this service, I planned to control all things related to this. The loadGridOptions function is started by gridCtrl and waits until the row and column data are loaded by the corresponding services. Then it initializes this gridOptions data object.
  • gridConfigService . This is a simple service that is used to load column definitions. There is no problem.
  • gridDataService This service is used to load row data using the loadRowData function. Another feature of this service: it simulates the current updates coming from the server ($ interval function). Problem 1 is here!
  • gridSettingsService . This service is used to apply some settings to the grid (for example, how to sort columns). Problem 2 is here!

Problem 1: I have a circular dependency between gridMainService and gridDataService . First gridMainService use gridDataService to load row data using a function:

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

But then gridDataService receives updates from the server and inserts some rows into the grid. Thus, it should use 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); 

So, here I came across the first CD.

Problem 2: I have a circular dependency between gridMainService and gridSettingsService . Firstly, launch gridMainService when the grid is initially loaded, and then set the default settings using the following function:

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

But to make some changes to the gridSettingsService, you need access to the gridMainService and its gridOptions objects to apply the settings:

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

Question: How can I solve these cases of CDs correctly? Because Mishko Hevery's blog was pretty short and good, but I was not able to apply my strategy to my business.

And at present, I don’t really like the manual input approach, because I have to use it a lot, and the code looks a bit fuzzy.

Please note: I only prepared a demo of my large project. You can probably advise putting all the code in a gridDataService and you are ready. But I already have 500 LOC in this service, if I combine all the services, it will be a nightmare ~ 1300 LOC.

+6
source share
3 answers

There are many solutions to such problems that depend on how you think. I prefer to think that each service (or class) has a purpose and some other service is required to achieve its goal, but its goal is one, clear and small. Let's see your code in this view.

Problem 1:

GridData strong>: Grid data is stored here. MainService comes here to get the necessary data, so we add this to mainService and we use the loadRowData function to get rowData data just like you do, but in $ interval you enter mainService inside gridData, but gridData doesn't need mainService to complete your goal (get items from the server).

I solve this problem using an observer design pattern (using $ rootScope). This means that I receive a notification when the data arrives, and the mainService arrives and receives it.

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()); }) [..] 

When using a real server, the most common solution is to use promises (not an observer pattern) such as loadRowData.

Problem 2 :

gridSettingsService . This service changes the settings of mainService, therefore it requires mainService, but mainService does not care about gridSettings when someone wants to change or find out that the internal state of mainService (data, form) must communicate with the mainService interface,

So, I remove the Preferences grid from gridMainService and provide an interface only for the callback function when the Grid is ready.

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; }); 

here is the full code: https://plnkr.co/edit/VRVANCXiyY8FjSfKzPna?p=preview

+2
source

You can enter a separate service that provides specific challenges, such as adding items to the grid. Both services will have a dependency on this api service, which allows the data service to abandon the dependency on the main service. This separate service will require your main service to register a callback that should be used when you want to add an item. The data service, in turn, will be able to use this 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); }); }; }); 

In the above example, there are two functions provided by the service. The insert function allows you to register a callback from your main function, which can be used later. The exec function will allow your data service to use the stored callback by passing a new element.

+1
source

First of all, I agree with other comments that the denouement seems a bit extreme; Nevertheless, I am sure that you know more than we need for the project.

I find it an acceptable solution to use a pub / sub (observer pattern) in which gridSettingsService and gridDataService do not directly update gridMainService , but instead raise a notification that gridMainService can connect and use to update.

Thus, using the plunker you provided, the changes that I would make would be as follows:

  • Add the $rootScope service to gridSettingsService , gridDataService and gridMainService .

  • From gridSettingsService and gridDataService stop manually entering gridMainService and instead just have a $broadcast notification with the associated data:

`` ``

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

`` ``

  1. In the gridMainService specified in the notifications and update using the transferred data:

`` ``

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

`` ``

And here is the updated plunker: https://plnkr.co/edit/pl8NBxU5gdqU8SupnMgy

+1
source

Source: https://habr.com/ru/post/1013829/


All Articles