Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems with circular dependency and OOP in AngularJS

AngularJS + OOP is kinda sexy feature to use

Hi, I'm successfully using OOP with AngularJs for some time already (first started with angularjs with oop inheritance in action), the provided approach allows you define your classes as angular services, which you can later extend or inherit from like that:

Application.factory('AbstractObject', [function () {
    var AbstractObject = Class.extend({
        virtualMethod: function() {
           alert("Hello world");
        },
        abstractMethod: function() { // You may omit abstract definitions, but they make your interface more readable
           throw new Error("Pure abstract call");
        }
    });

    return AbstractObject; // You return class definition instead of it's instance
}]);

Application.factory('DerivedObject', ['AbstractObject', function (AbstractObject) {
    var DerivedObject = AbstractObject.extend({
        virtualMethod: function() { // Shows two alerts: `Hey!` and `Hello world`
            alert("Hey!");

            this._super();
        },
        abstractMethod: function() {
            alert("Now I'm not abstract");
        }
    });

    return DerivedObject;
}]);

Plunker: http://plnkr.co/edit/rAtVGAsNYggBhNADMeoT

using the described approach gives you the ability to define classes that beautifully integrate into angular infrastructure. You get all sort of nifty features from two worlds - OOP and AngularJs. Dependency injection is free for your classes, and it makes your classes simple, allows putting a lot of boilerplate controller code into some base class that can be later reused.

However

AngularJs infrastructure blocks previously described approach from spreading it's wings on all 100%. The problem occurs when you try to define recursive class definitions (i.e. recursive aggregation), say you have two class definitions like Blog and Tag

Application.factory('Blog', ['Tag', function (Tag) {
    var Blog = Class.extend({
        tags: function() {
            return this.tags;
        }
    });

    return Blog;
}]);

Application.factory('Tag', ['Blog', function (Blog) {
    var Tag = Class.extend({
        Blogs: function() {
           return this.blogs;
        }
    });

    return Tag;
}]);

It won't work because both Blog and Tag are self-referencing themselves causing circular dependency.

P.S

The last thing, I have found kinda ugly solution that solves my problem in my specific case but doesn't work in general and as I said, it isn't pretty:

Application.factory('BlogNamespace', [function () {
    var Blog = Class.extend({
        tags: function() {
            return this.tags;
        }
    });

    var Tag = Class.extend({
        Blogs: function() {
           return this.blogs;
        }
    });

    return {
        Tag: Tag,
        Blog: Blog
    };
}]);

Question

The above fix won't work because namespaces may also be a subject of circular dependency. This means that it isn't solution to described problem but rather one level deeper problem now.

Any suggestions on how it is possible to solve described problem in general case?

like image 823
Lu4 Avatar asked Oct 13 '13 10:10

Lu4


People also ask

What's wrong with circular dependencies?

and, yes, cyclic dependencies are bad: They cause programs to include unnecessary functionality because things are dragged in which aren't needed. They make it a lot harder to test software. They make it a lot harder to reason about software.

How do you fix a circular dependency problem?

There are a couple of options to get rid of circular dependencies. For a longer chain, A -> B -> C -> D -> A , if one of the references is removed (for instance, the D -> A reference), the cyclic reference pattern is broken, as well. For simpler patterns, such as A -> B -> A , refactoring may be necessary.

What is circular dependency error 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.

Why are circular dependencies bad in Javascript?

However, from a pure design point of view, circular referencing is still a bad thing and a code smell. Circular referencing implies that the 2 objects referencing each other are tightly coupled and changes to one object may need changes in other as well. There is no one way to avoid circular reference in JS.


3 Answers

A circular dependency is always the sign of mixing of concerns, which is a really bad thing. Miško Hevery, one of the authors of AngularJS, explains a nice solution on his awesome blog. In short, you probably have a third service hidden somewhere, which is the only part of your code really needed by the two others.

like image 197
Blackhole Avatar answered Oct 17 '22 03:10

Blackhole


I'm answering my own question just because I've found a technical way of resolving the issue that I have originally posted about. But before that, I strongly encourage you to use Blackhole's suggestion since it allows solving a broader set of problems which are usually caused by bad architecture. Please prefer using his approach first, and return to current one in case that you know what you are doing.

So here goes:

You can use $injector service and inject required definitions at run-time, which is legal from technical point of view, but again according to this post (hard to imagine that it is written in 2008), this is like a black magic, do that and it will strike you back:

Application.factory('Blog', ['$injector', function ($injector) {
    var Tag = $injector.get('Tag'); // Here is your tag

    ...    
}]);

Application.factory('Tag', ['Blog', function (Blog) {
    ...
}]);

Edit

It turned out that current approach is an example of Service Locator pattern, which is IoC Antipattern.

like image 29
Lu4 Avatar answered Oct 17 '22 03:10

Lu4


LAST RESORT: NOT ENCOURAGED

In my case the best way to get around a circular-dependency-problem like this in angular, is to trigger function-calls via $rootScope-broadcasts. The other service can then listen to this broadcast and react with the desired function-call. It may not be the most elegant solution but in some cases where the interaction between the services is mainly one-directional anyways, it may be a reasonable alternative. (note that this also allows return-values to be passed back to the broadcasting function via callbacks only)


A pseudo-example of this would be:

angular.module('myApp').factory('service1', ["$rootScope",
  function($rootScope) {
    function func1() {
      // do something
    }
    $rootScope.$broadcast("callFunc2"); // calls func2 from service 1

    return {
      func1: func1
    }
  }
]);
angular.module('myApp').factory('service2', ["service1", "$rootScope",
  function(service1, $rootScope) {
    function func2() {
      // do something
    }
    service1.func1();  // calls func1 from service 2
    $rootScope.on("callFunc2", func2);
  }
]);
like image 1
Lars Frölich Avatar answered Oct 17 '22 03:10

Lars Frölich