63

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?

Community
  • 1
  • 1
Lu4
  • 14,873
  • 15
  • 79
  • 132

3 Answers3

70

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.

Blackhole
  • 20,129
  • 7
  • 70
  • 68
  • Blackhole, it's very interesting post you've provided, my case with `Tag` and `Blog` proves that Misko is right, because these are sort of database entities that relate one to another as N:M and in relational bases world this kind of situation is also resolved by representing third relational table that holds reference to both `Tag` and `Blog` such as `BlogTags` – Lu4 Oct 13 '13 at 13:07
  • So far so good... The flight is stable, described approach really helps with identifying problematic implementation points. – Lu4 Oct 14 '13 at 19:44
  • I think that your approach is correct and having circular deps in code is kinda bad thing to do, so the answer goes to you – Lu4 Nov 18 '13 at 23:48
  • @Lu4 did you solve this by adding a `BlogTags` factory that `Blog` and `Tag` get injected with? I'd really rather not add "joining table" classes into my code base if I can help it... If my only other option is circular dependencies using `$injetor.get` as you pointed out, I might just stick with that. – Brett Dec 09 '14 at 07:04
  • Yeah I have a service/model that requires a count (really a hierarchy map of n deep structures) from other models to generate "widgets" to display that data. The containing data has all business logic separate, but to generate new widget instances bound to those models, I need to first lookup the meta data before generating them... I would feel safe doing $injector.get for this use case. – FlavorScape Jan 05 '16 at 19:32
58

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.

Community
  • 1
  • 1
Lu4
  • 14,873
  • 15
  • 79
  • 132
  • I would suggest not to worry much about the anti-pattern comments/opinions on Service Locator, as not everyone (including me) agrees with it. In the other hand what you are trying to do here is just modeling objects and their relations using an Angular-specific mechanism to achieve it (whose original purpose is other), and therefor you should not have to worry about removing cycling dependencies (unless you cannot find a workaround, which you already found). – Rodrigo Quesada May 05 '15 at 14:29
  • 2
    @RodrigoQuesada your suggestion is a suggestion of a truly young and passionate developer :) It's really cool that you subject dogmas and paradigms, it means you pick your own way. However to me it looks funny because I'm already too old to follow it. Please don't take my words as an insult, rather treat them as the voice of experience, the sound of your future. I find multitude reasons to worry about your proposition. One of them is the fact that I know that they were written by blood of many innocent developers. And I don't need everybody to agree with this fact, I just need the smart guys – Lu4 May 05 '15 at 19:49
  • Sure, the thing is that in this case (which is that of using AngularJS' providers for implementing OOP) you should not worry about changing your OO model (and thus stray away from your mental model?) when the reason for it is that you are following advises that were meant for something else. If you were implementing an application using any (yes, any) dependency injection framework in a language originally design with OOP in mind, you would not have this kind of issues, because you would not be trying to "inject" a class definition (you would probably just use an import statement). – Rodrigo Quesada May 06 '15 at 07:38
  • I agree that JS wasn't initially mend for OOP and it is more a lack rather than benefit. Even in such poorly supported situation, by not following rules of OOP you will not be able to formalize the design of your software, will not be able to analyse it using typical software development tool set. You will eventually shoot yourself into leg, your software will contain massive amount of similar but not reusable and non-refactorable code. You will loose control over your code as soon as your team size will exceed 3 developers. This is really serious limitation, business can't afford it... – Lu4 May 06 '15 at 10:52
  • 2
    This doesn't work, $injector is literally what reports the cyclical dependencies to begin with http://jsfiddle.net/iamwhitebox/1z2kua70/ – Jeff Voss Sep 13 '15 at 19:06
  • 3
    @whitebox It doesn't work for unit tests, but if you're using it after the module is first initialized (e.g. using it inside a function in the module) it works fine. I think Lu4 simplified the case when he was writing the answer so that it would be short. – RevanProdigalKnight Oct 01 '15 at 18:35
  • 1
    I have a use case where i'd feel safe doing this. Have models/services that are all self-contained logically. But I have generic containers that have to be generated and know which models to bind to. These views can represent all models (that adhere to a specific Interface) but I need my "widget" model to have access to each model to retrieve their hierarchy schema up front (so we know how many widgets to make in the first place). I can't have my widget service be agnostic to other models/services. – FlavorScape Jan 05 '16 at 19:48
  • Tnx, I tried a few other ways to do it, but so far this is the only one that works and is not that bad :( :) – zetacu Sep 12 '17 at 21:29
1

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);
  }
]);
  • @Lu4 any constructive criticism? – Lars Frölich Mar 01 '17 at 14:41
  • I thought I've reached a limit in "darkness" when posted my answer to my own question (I'm referring to "dark side" joke in the comments), but now I see clearly my answer shines bright like electric 100w bulb compared to yours ))) – Lu4 Mar 01 '17 at 18:26
  • Seriously this kind of code (i.e. the `$broadcast`) was never mend to be used extensively in production (I mean the way you propose it) that's why it has this `$` dollar sign before it. `$broadcast` is one of the biggest design flaws that were ever introduced to angular. It gained popularity among embryo community only due to lack of understanding / documentation / examples for first versions of angular. For the most part nearly every use-case that involves `$broadcast` can be rewritten into direct function call. The use of `$broadcast` is a sign a problem in your app architecture. – Lu4 Mar 01 '17 at 18:29
  • The only thing that `$broadcast` does well is makes your code opaque and unpredictable. Looking at someone's code you never know which event you need to subscribe to. You can never be sure that you have the necessary subscribers subscribed. And when the event happens you never know who initiated it. It is always close to magic, the black, black magic. Don't use it, it will make your project end up bad, real bad. – Lu4 Mar 01 '17 at 18:34
  • Another thing is the "factory" it is used like an instance object, this is the second largest problem the angular has. The problem with angular's service "pattern" is the fact that it is just a singleton (due to way DI works in angular) And building your application architecture using singleton instance objects will result in naive application architecture. There are tons of other patterns that may and should become useful during development for example go for GoF. But who am I to give advices, pick your own route, hit on your own bumps like I did and then judge people's answers :) – Lu4 Mar 01 '17 at 18:47
  • 2
    @Lu4 : oh well, lessons learned ^^ I guess now we have a good anti-example for how not to solve this problem.. I'll review my architecture and see if I can maybe avoid the problem altogether. So yes: welcome to the dark side. – Lars Frölich Mar 03 '17 at 08:49