Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular: circular dependency of specific case

Some time ago, I started to refactor my code of the main project, decoupling the business logic from controllers to services, according to guidelines. Everything went well, until I faced the problem of circular dependency (CD). I read some resources about this problem:

Question 1 on Stack Overflow

Question 2 on Stack Overflow

Miško Hevery blog

Unfortunately, for me it is not clear now, how can I solve the problem of CD for my project. Therefore, I prepared a small demo, which represents the core features of my project:

Link to Plunker

Link to GitHub

Short description of components:

  • gridCtrl Does not contain any business logic, only triggers dataload and when the data is ready, show grid.
  • gridMainService This is the main service, which contains object gridOptions. This object is a core object of grid, contains some api, initialized by framework, row data, columns headers and so on. From this service, I was planning to control all related grid stuff. Function loadGridOptions is triggerd by gridCtrl and waits until the row data and column definitions are loaded by corresponding services. Then, it initializes with this data gridOptions object.
  • gridConfigService This is simple service, which used to load column definitions. No problem here.
  • gridDataService This service used to load row data with function loadRowData. Another feature of this service: it simulates the live updates, coming from server ($interval function). Problem 1 here!
  • gridSettingsService This service is used to apply some settings to the grid(for example, how the columns should be sorted). Problem 2 here!

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

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

But then, gridDataService receives the updates from server and has to insert some rows into grid. So, it has to 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 faced first CD.

Problem 2: I have a circular dependency between gridMainService and gridSettingsService. First, gridMainService triggering, when the grid is initially loaded and then it sets up default settings by the following function:

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

But, to make some changes, gridSettingsService need an access to the gridMainService and its object gridOptions in order to apply 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 CD cases in proper way? Because, Miško Hevery Blog was quite short and good, but I have not managed to apply his strategy to my case.

And currently, I don't like an approach of manual injection a lot, because I will have to use it a lot and the code looks a little bit fuzzy.

Please note: I prepared only a demo of my big project. You can probably advice to put all code in gridDataService and ready. But, I already has a 500 LOC in this service, if I merge all services, it will be a ~1300 LOC nightmare.

like image 356
omalyutin Avatar asked Jan 06 '17 16:01

omalyutin


People also ask

How does angular resolve cyclic dependency?

In some scenarios, we can duplicate the required code and solve circular dependency. Or we can create a new service and move that code to that new service to avoid circular dependency.

How does angular detect circular dependency?

By running a cli command npx madge --circular --extensions ts ./ we can quickly get a list of circular dependencies of all . ts files in current directory and its subdirectories. That's it! Now you see where you have circular dependencies and can go and fix it.

What is cyclic dependency in angular?

A cyclic dependency exists when a dependency of a service directly or indirectly depends on the service itself. For example, if UserService depends on EmployeeService , which also depends on UserService . Angular will have to instantiate EmployeeService to create UserService , which depends on UserService , itself.

How do you solve a circular dependency reaction?

To resolve circular dependencies: Then there are three strategies you can use: Look for small pieces of code that can be moved from one project to the other. Look for code that both libraries depend on and move that code into a new shared library. Combine projectA and projectB into one library.


2 Answers

In these kind of problems there are many solutions, which depend by the way you are thinking. I prefer thinking each service (or class) has some goal and needs some other service to complete its goal, but its goal is one, clear and small. Let’s see your code by this view.

Problem 1:

GridData: Here lives the data of grid. MainService comes here to get the data that needs, so we inject this to mainService and we use the loadRowData function to get the rowData data as you do, but in $interval you inject mainService inside gridData but gridData doesn’t need mainService to end its goal (get items from server).

I solve this problem using an observer design pattern, (using $rootScope). That means that I get notified when the data are arrived and mainService come and get them.

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 a real server is used, the most common solution is to use the promises (not observer pattern), like the loadRowData.

Problem 2:

gridSettingsService: This service change the settings of mainService so it needs mainService but mainService doesn’t care about gridSettings, when someone wants to change or learn mainService internal state (data, form) must communicate with mainService interface.

So, I delete grid Settings injection from gridMainService and only give an interface to put a callback function for when 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 the full code: https://plnkr.co/edit/VRVANCXiyY8FjSfKzPna?p=preview

like image 89
Thomas Karachristos Avatar answered Oct 05 '22 12:10

Thomas Karachristos


You can introduce a separate service that exposes specific calls, such as adding items to the grid. Both services will have a dependency to this api service, which allows for the data service to drop its 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 make use of 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 made available from the service. The insert function will allow for you to register a callback from your main function, that can be used at a later time. The exec function will allow for your data service to make use of the stored callback, passing in the new item.

like image 25
Brandyn Bayes Avatar answered Oct 05 '22 12:10

Brandyn Bayes