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?
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.
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
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() ;)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With