Home > Enterprise >  Algorithm to resolve circular dependencies?
Algorithm to resolve circular dependencies?

Time:11-11

I would like to create a module system whereby you can load dependencies in a circular fashion. The reason is, circular dependencies arise all over the place, like in Ruby on Rails, in the relationship between model classes:

# app/models/x.rb
class X < ActiveRecord::Base
  has_many :y
end

# app/models/y.rb
class Y < ActiveRecord::Base
  belongs_to :x
end

The way this generically works in Ruby on Rails models, at least, is it first loads all models, with the dependency being a symbol or "string" at first. Then a centralized manager takes all these objects and converts the strings into the corresponding classes, now that the classes are all defined. When you set the relationships of the first class to their corresponding classes, the rest of the classes are still relating to strings, while this first class relates to classes, so there is an interim period where some of the models are fully resolved to classes, while others are still as strings, as the algorithm is running and has not reached completion. Once all the strings are resolved to their corresponding classes, the algorithm is complete.

I can imagine a world where it gets much more complex, creating loops that are not one-to-one circular dependencies, but have many layers in between, creating a 5-node loop sort of thing. Or even one where there are back-and-forth dependencies between modules, such as:

# file/1.rb
class X
  depends_on A
  depends_on B
end
class Y
  depends_on B
end
class Z
  depends_on Q
end

# file/2.rb
class A
  depends_on Y
end
class B
  depends_on Z
end

# file/3.rb
class Q
  depends_on X
end

To resolve this:

  1. Load all files *.rb, treat depends_on as strings. Now we have the definitions of each class in their corresponding modules, but with the depends_on as strings instead of the desired classes.
  2. Iterate through files and resolve classes.
  3. For file 1.rb, resolve X... etc.
  4. X depends on A, so associate with 2/A.
  5. X also depends on B, so associate with 2/B.
  6. Y depends on B, so associate with 2/B.
  7. Z depends on Q, so associate with 3/Q.
  8. A depends on Y, so associate with 1/Y.
  9. B depends on Z, so associate with 1/Z.
  10. Q depends on X, so associate with 1/X.
  11. Done.

So basically, there are two "rounds". First round is load all the files, and initialize the classes. Second round is associate the class members with corresponding other classes. Doesn't matter the order.

But can it get any more complex? Requiring more than two rounds to resolve such circular dependencies? I'm not sure. Take for example something like this:

# file/1.rb
class A < A_P
  class AA < A_P::AA_P
    class AAA < A_P::AA_P::AAA_P

    end
  end
end
class B < B_Q
  class BB < B_Q::BB_Q
    class BBB < B_Q::BB_Q::BBB_Q

    end
  end
end

# file/2.rb
class A_P
  class AA_P < B
    class AAA_P < B::BB

    end
  end
end
class B_Q
  class BB_Q < A
    class BBB_Q < A::AA

    end
  end
end

In this contrived case, you have:

  1. A (file/1) depending on A_P (file/2), then:
  2. AA (file/1) depending on A_P and AA_P, then:
  3. AA_P (file/2) depending on B (file/1), then:
  4. B (file/1) depending on B_Q (file/2), etc....

That is, it seems like something weird is going on. I'm not sure, my head starts going in knots.

  1. You can't define what class A is extending until class A_P is fully resolved. You can't define what class AA is until class AA_P is fully resolved, which depends on B being resolved, which depends on B_Q being resolved. Etc..

Is it possible to resolve such circular dependencies? What is the general algorithm for resolving arbitrarily complex circular dependencies? Such that, the end is that all the circular dependencies are wired up with the actual value, not a string or other such symbol representing the actual value. The end result of resolving circular dependencies are that every reference should refer to the actual object.

Is it always just a simple two-pass algorithm, first loading the base objects, then resolving their dependencies converting the "strings" of the dependencies to the base objects in the set?

Can you come up with an example where it requires more than a simple two-pass algorithm? And then describe how the algorithm should work in resolving the circular dependencies in that case? Or prove/explain how it is certain that only a simple two-pass algorithm is required?

Another example might be:

// ./p.jslike
import { method_x } from './q'
import { method_y } from './q'

function method_a() {
  method_x()
}

function method_b() {
  console.log('b')
}

function method_c() {
  method_y()
}

function method_d() {
  console.log('d')
}

// ./q.jslike
import { method_b } from './p'
import { method_d } from './p'

function method_x() {
  method_b()
}

function method_y() {
  method_b()
}

I guess that would also be two-pass.

CodePudding user response:

The answer to your question depends on what you mean by "resolve".

Consider the following situation:

You have three classes, A, B and C, which depend on each other in the circular way.

After the first pass (loading), you have access to all the information about each class, that is written in the source files.

Now, let's say you want to fully "resolve" class A. Pretend you don't care about other classes for now.

Given just the information from files, is it possible to perform the desired "resolve" just for the A?


If the answer is no, then the answer to your question is no as well.

If the answer is yes, then it means that two passes are sufficient to fully resolve every class. You can do this either recursively or iteratively, and you'd probably want to cache fully resolved classes to avoid resolving them multiple times, although, it's not necessary for correctness (it's purely a performance optimization).

  • Related