Search code examples
javascriptrequirejsclassloadercircular-dependency

requirejs circular dependencies and function objects


Is it possible to differentiate between dependencies which are needed on structure build (like inheritance) and runtime dependencies (within a method call).

A little example:

2 "classes": Father and Child which depend on each other

Father.js

define(['Child'], function (Child) {

    function Father() {};    

    Father.prototype.childs = [];

    Father.prototype.addChild = function (c) {
        if (!(c instanceof Child)) {
            alert("nope");
        }
    }

    return Father;
});

Child.js

define(['Father'], function(Father){

   function Child(){
       this.father = [];
   }

   Child.prototype.setFather = function(f){
       if(!(f instanceof Father)){
           alert("false");
       }
   }

   return Child;
});

and an app.js

requirejs(['Child', 'Father'], function(Child, Father){
    var c = new Child();
    var f = new Father();
    c.setFather(f);
    f.addChild(c);
});

When using export you can only extend an object (if i'm correct). So is it possible to build a structure like this ?

What i actually try to do: have an automatic "class"-loading (seperated files), which loads all dependencies within a bigger model, which has some circular dependencies. Some dependencies needed right away (inheritance) and some are only needed after object initiation. And I cant find a nice solution for my problem.


Solution

  • This is (IMO) a vary valid question, puzzled me since yesterday. What I would consider if I were you, in preference order:

    1. Embrace ducktyping. A useful Child class may have methods like play() and cry(), so the "if it walks like a duck and quacks like a duck..." becomes "if it plays like child and cries like a child, then it is a child":

      // Father.js
      define([], function() {
      
          function Father() {};    
      
          Father.prototype.childs = [];
      
          Father.prototype.addChild = function (c) {
              if( !c || typeof(c.play) !== 'function' || typeof(c.cry) !== 'function' ) {
                  alert("nope");
              }
          }
      
          return Father;
      });
      

      Another benefit of this approach (in the general case) is that you are actually programming against the equivalent of interfaces in Javascript. I.e. the thought process is "What am I going to need a Child for? -Playing and crying. -Then it is enough for me that the object I get has these 2 methods. I do not care about the exact implementation of it."

    2. This is technical and ugly, but (at least IMO) acceptably ugly: You can introduce a 3rd dependency to break the cycle as is common with cycles. I.e.:

      // Father.js
      define([], function() {
      
          function Father() {};    
      
          Father.prototype.childs = [];
      
          return Father;
      });
      
      // Child.js
      define(['Father'], function(Father){
      
          function Child(){
              this.father = [];
          }
      
          Child.prototype.setFather = function(f){
              if(!(f instanceof Father)){
                  alert("false");
              }
          }
      
          return Child;
      });
      
      // FatherAugmentor.js (any ideas for a better name?)
      define(['Father', 'Child'], function(Father, Child) {
          Father.prototype.addChild = function (c) {
              if (!(c instanceof Child)) {
                  alert("nope");
              }
          }
      });
      

      A catch: you must make sure that the FatherAugmentor is required from somewhere! Alternatively, you can play with names (ugly again), so that the FatherAugmentor from above becomes the Father and the Father from above is renamed to, e.g. FatherBase:

      // FatherBase.js
      define([], function() {
          // exactly the same as `Father.js` from above
      });
      
      // Child.js
      define(['FatherBase'], function(Father){
          // exactly the same as `Child.js` from above
      });
      
      // Father.js
      define(['FatherBase', 'Child'], function(Father, Child) {
          // exactly the same as `FatherAugmentor.js` from above
      });
      

      With this you make sure that anybody that requests the Father will get the full class, but you have to be careful to use the FatherBase in the files that are parts of the cycle (i.e. Child.js and Father.js).

    3. One could consider solutions involving a namespace object (i.e. return a family object that contains the constructors as family.Father and family.Child), but I think it gets too ugly.