1

I've been spoiled by Rail's autoloading of missing constants. In Ruby, if I have two classes, one nested inside the other but in different files, how do I require them since both depend on each other (circular dependency).

# user.rb
class User < ActiveRecord::Base
   serialize :preferences, User::Preferences
end

# user/preferences.rb
class User::Preferences
end

# user_spec.rb    
require 'user'
require 'user/preferences'

Note: I have not required the Rails environment.

If I try and load User first, the code fails because it does not know about User::Preferences yet. If I load "user/preferences" first, it fails when it loads User because the existing User class does not subclass ActiveRecord.

I have a suspicion I need to remove the circular dependency or, if possible, make serialize lazy load the class by passing a string 'User::Preferences' which is turned in to a constant when needed.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Kris
  • 19,188
  • 9
  • 91
  • 111
  • This is not a duplicate, it has the same topic, but different question. In the "duplicate" the dependency is within a method call, not the class body like my example code. In the "duplicate" there actually isn't a problem, note in the answer: "Ruby isn't C++, it won't complain about FooSub.SOME_CONSTANT until you call Foo#foo() ;)". For me this is not true since my dependency is in the class body it is called as soon as the class is loaded, not at some future point after the dependency has been loaded. – Kris Apr 02 '13 at 08:38

2 Answers2

2

One hack I have is to create an empty User class inheriting from ActiveRecord::Base in user/preferences.rb:

class User < ActiveRecord::Base; end

class User::Preferences
end
Kris
  • 19,188
  • 9
  • 91
  • 111
  • I don't regard this as a "hack". The fact that you can open a class at any time is one of the best features of Ruby. You are saying here, "OK, I hereby assert there is a User class"; if this User class is known, the assertion does no harm, and if it isn't, it causes the class to be known. – matt Apr 01 '13 at 02:02
  • This sounds like a knee-jerk reaction to having designed the classes' files poorly. I'd do something like @dbenhur's suggestions. Create a stub that the other two files require. – the Tin Man Apr 01 '13 at 02:02
  • @theTinMan I don't think it's the classes but the distribution of info across multiple files that's giving the OP problems. And there can be good reasons for that. – matt Apr 01 '13 at 02:03
  • @matt I consider it a hack because I'm only putting the empty class to make dependency loading work. – Kris Apr 01 '13 at 12:44
  • @theTinMan If Prefrences was nested inside User in the same file, would you still consider it a smell? – Kris Apr 01 '13 at 12:45
  • No, that's not smell at all, it's a standard way of handling a supporting class, especially one that doesn't need to be visible outside the User class. You can expose accessors to the Preferences very cleanly. The way I look at objects is there is a main one (`User`) and attributes of that are internal to it. If you need to have a sub-class to encapsulate a bunch of other related attributes, put them inside the main class. You can put `require 'user/preferences'` inside the User class definition and keep the preferences code as a separate file that way. My thumb's up on going that way! – the Tin Man Apr 01 '13 at 15:03
0

Rather than wire knowledge of User's implementation into User::Preferences you could put the stub declaration in a common base, like so:

# user_base.rb
class User < ActiveRecord::Base; end

# user.rb
require 'user_base'
require 'user/preferences'

class User
   serialize :preferences, User::Preferences
   ...
end

# user/preferences.rb
require 'user_base'

class User::Preferences
end

Alternatively, you could move User::Preferences into an independent module namespace such as ModelHelper::User::Preferences. I think I prefer this solution. The fact that you have a circular dependency is a code smell and the only thing causing it is the reuse of User class as a namespace container for User::Preferences.

dbenhur
  • 20,008
  • 4
  • 48
  • 45
  • 2
    +1 "The fact that you have a circular dependency is a code smell". Agreed. – the Tin Man Apr 01 '13 at 01:57
  • Having the base_user.rb looks the same as the "hack" I posted, i.e creating an empty class with the correct inheritence and re-opening it later. I don't like the idea of adding a `ModelHelper` namespace. Preferences isn't a helper, its a value object specific to `User`, so my thinking is, it should be nested in `User`. I do have a feeling this could be a smell however since there is no obvious solution. – Kris Apr 01 '13 at 12:48
  • In Ruby, the module namespacing is essentially arbitrary. There's no semantic significance to `Foo::Bar` vs just `Bar` or `Baz::Bar` in terms of `Bar`s relationship to `Foo`. `Bar` is not private to `Foo` nor does it have any special implied or explicit access to `Foo` and vice-versa. However, by defining `Bar` within `Foo` you introduce an extra requirement that `Foo` be defined which requires that Bar knows specifics about Foo to properly do that (class or module? what superclass) since it can't just `require 'foo'` when `Foo` has a real semantic dependency on `Bar`. – dbenhur Apr 01 '13 at 16:32
  • The forward declaration is indeed a "hack" _similar_ to yours, but also a requirement given the dependencies you've arranged. Moving it to a common base makes it slightly less so and more DRY. Now there's only one place to say whether `User` is a class or module and what its superclass may be. – dbenhur Apr 01 '13 at 16:36