Let's say we have two classes, Foo and Foo Sub, each in a different file, foo.rb and foo_sub.rb respectively.
foo.rb:
require "foo_sub"
class Foo
def foo
FooSub.SOME_CONSTANT
end
end
foo_sub.rb:
require "foo"
class FooSub < Foo
SOME_CONSTANT = 1
end
This isn't going to work due to the circular dependency - we can't define either class without the other. There are various solutions that I've seen. Two of them I want to avoid - namely, putting them in the same file and removing the circular dependency. So, the only other solution I've found is a forward declaration:
foo.rb:
class Foo
end
require "foo_sub"
class Foo
def foo
FooSub.SOME_CONSTANT
end
end
foo_sub.rb
require "foo"
class FooSub < Foo
SOME_CONSTANT = 1
end
Unfortunately, I can't get the same thing to work if I have three files:
foo.rb:
class Foo
end
require "foo_sub_sub"
class Foo
def foo
FooSubSub.SOME_CONSTANT
end
end
foo_sub.rb:
require "foo"
class FooSub < Foo
end
foo_sub_sub.rb:
require "foo_sub"
class FooSubSub < FooSub
SOME_CONSTANT = 1
end
If I require foo_sub.rb, then FooSub is an uninitialized constant in foo_sub_sub.rb. Any ideas how to get around this without putting them in the same file nor removing the circular dependency?
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 Foo
2 def initialize(foo_sub:)
3 end
4 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 Foo
2
3 attr_reader :foo_sub
4
5 def initialize(foo_sub:)
6 @foo_sub = foo_sub
7 end
8 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 FooSub
2 SOME_CONSTANT = 1
3 end
and you can add a method to your Foo
class that has access to #SOME_CONSTANT:
1 class Foo
2
3 attr_reader :foo_sub
4
5 def initialize(foo_sub:)
6 @foo_sub = foo_sub
7 end
8
9 def foo
10 foo_sub.SOME_CONSTANT
11 end
12 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.