Circular Dependencies in Ruby Circular Dependencies in Ruby ruby ruby

Circular Dependencies in Ruby


If you need to access a subclass from a superclass then there's a good chance that your model is broken (i.e. it should be one class).

That said, there are a couple of obvious solutions:

1) just create a file that requires the foo files:

all_foos.rb:

require "foo.rb"require "foo_sub.rb"

and remove the requires from foo.rb and foo_sub.rb.

2) remove the require from foo.rb

3) remove the require from foo_sub.rb and put the require in foo.rb after the class definition.

Ruby isn't C++, it won't complain about FooSub.SOME_CONSTANT until you call Foo#foo() ;)


Another decent option is to use the autoload feature of Ruby.

It works like this:

 module MyModule      autoload :Class1, File.join(File.dirname(__FILE__), *%w[my_module class1.rb])      autoload :Class2, File.join(File.dirname(__FILE__), *%w[my_module class2.rb])      # Code for MyModule here end

and is described well here:

http://talklikeaduck.denhaven2.com/2009/04/06/all-that-you-might-require


Sandi Metz explains one solution to this problem and how to solve it really nicely in her book Practical Object-Oriented Design in Ruby (POODR).

What she suggests (and I'm inclined to agree with as its worked the best for me so far), is to inject the sub-class FooSub into the master class Foo.

This would be done in foo.rb with:

1   class Foo2     def initialize(foo_sub:)3     end4   end

to maintain clean code, and keep it easily changeable, you would then wrap the foo_sub in a wrapper method so your class now looks like this:

1   class Foo23     attr_reader :foo_sub45     def initialize(foo_sub:)6       @foo_sub = foo_sub7     end8   end

(here, the attr_reader is setting up a method called foo_sub and then whatever is passed into the value of the initialize hash is an instance of foo_sub, therefore @foo_sub (line 6), can be set to the value of the method foo_sub).

You can now have your FooSub class with no requires, making it independent of anything:

1   class FooSub2     SOME_CONSTANT = 13   end

and you can add a method to your Foo class that has access to #SOME_CONSTANT:

1   class Foo23     attr_reader :foo_sub45     def initialize(foo_sub:)6       @foo_sub = foo_sub7     end8     9     def foo10      foo_sub.SOME_CONSTANT11    end12  end

In actuality, with this, you're setting up a method that returns the instance of foo_sub @foo_sub (that is injected at the initialize), with the method #SOME_CONSTANT appended onto it. Your class just expects whatever is injected in at the initialize to respond to #SOME_CONSTANT. SO for it to work you would have to inject you FooSub class when setting up Foo in a REPL (e.g IRB or PRY):

PRY[1]>   require 'foo'[2]>   => true[3]>   require 'foo_sub'[4]>   => true[5]>   foo_sub = FooSub.new[6]>   => #<FooSub:0x007feb91157140>[7]>   foo = Foo.new(foo_sub: foo_sub)[8]>   => #<Foo:0x007feb91157735 @foo_sub=FooSub:0x007feb91157140>[9]>   foo.foo[10]>  => 1

if, however, you injected something else, you'd end up with:

PRY[1]>   require 'foo'[2]>   => true[3]>   require 'foo_sub'[4]>   => true[5]>   foo_sub = FooSub.new[6]>   => #<FooSub:0x007feb91157140>[7]>   foo = Foo.new(foo_sub: 'something else as a string')[8]>   => #<Foo:0x007feb91157735 @foo_sub='something else as a string'>[9]>   foo.foo[10]>  => UNDEFINED CONSTANT #SOME_CONSTANT ERROR MESSAGE

I don't know what the actual error message would read on line 10 but think along those lines. This error would of occurred because you'd have effectively tried to run the method #SOME_CONSTANT on the string 'something else as a string' or 'something else as a string'.SOME_CONSTANT which would obviously not work.