62
votes

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?

3

3 Answers

70
votes

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.

58
votes

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.

1
votes

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