I’ve been meaning to finish a post on this subject for ages. As I probably won’t finish it in its current incarnation, I thought I’d at least get something up here.
Active Record associations are brilliant for quickly putting together an object graph. They’re not so brilliant for testing, or keeping the code particularly tidy. They quickly lead to (and even encourage) train-wreck expressions in your code. Diving straight into an example:
class Person < ActiveRecord::Base has_many :hobbies end class Hobby < ActiveRecord::Base belongs_to :person end
Because the hobbies object is publicly accessible from a person object, it allows us to reach in and touch things we shouldn’t.
cycling = Hobby.new
# We shouldn't manipulate the hobbies object directly...
chris = Person.new
chris.hobbies.add(cycling) # Uh oh - touching things we shouldn't
# ...rather, it'd be better to add a wrapper method for the functionality we actually need
class Person < ActiveRecord::Base
def add_hobby(hobby)
hobbies.add(hobby)
end
end
chris = Person.new
chris.add_hobby(cycling) # Ahh, that's much better
The revised Person class above follows the law of demeter. As well as making the code look a little neater (no train-wrecks to see here), it makes it easier to test. To illustrate the testing point, I’m going to extend the association in this example.
# We end up with a large and not very clear test
class Person < ActiveRecord::Base
has_many :hobbies do
def find_favourite
find(:first, :conditions => ['favourite = true'])
end
end
end
class MyContrivedTest < Test::Unit::TestCase
def test_should_find_my_favourite_hobby
hobbies = mock
hobbies.expects(:find_favourite)
person = stub(:hobbies => hobbies)
hobby_finder = HobbyFinder.new
hobby_finder.find_favourite(person)
end
end
# This way, we end up with a slightly cleaner looking class (imo) and a smaller, cleaner test
class Person < ActiveRecord::Base
has_many :hobbies
def find_my_favourite_hobby
hobbies.find(:first, :conditions => ['favourite = true'])
end
end
class MyContrivedTest < Test::Unit::TestCase
def test_should_find_my_favourite_hobby
person = mock
person.expects(:find_my_favourite_hobby)
hobby_finder = HobbyFinder.new
hobby_finder.find_favourite(person)
end
end
Interestingly enough, if we had test driven the development of the above code, we would almost certainly have ended up with something similar (interface-wise) to the example that has a wrapper around the association proxy.
So, with all of this in mind, I’ve hacked together a real quick plugin that adds a corresponding private method for each of the four (hasone, has_many, belongs_to and has_and_belongs_to_many) associations. Using the _private alternative will do exactly the same as the original method but it will make all of the dynamically created methods private. So. It’ll still be possible to write the wrapper around the association (as we did in the test above) but it won’t be possible to reach in and touch things that we shouldn’t.1
There is at least one problem (there may be many more I’m not aware of) with using the private association methods. If you have validation set-up for one of the associations (e.g. validatesassociated) it will fail. This is because validation happens from outside the instance of the object and therefore needs to access the association. Although I haven’t thought this through fully, I’m actually wondering if validation (in its current form) takes a bit of a back seat under this new world order. I think that one of the reasons validation is on the object itself is because there are many many ways to create the said object. However, using these private associations and disabling the persistence methods (create, save, update etc) on an associated object may allow us to have a single point of object creation and persistence. If we had that, then we wouldn’t need the Active Record validations anyway. Just a thought…
Just one final note. Dave Astels has a good article about encapsulation in rails, as referred to by Ben
1 Ok. That is, of course, a blatant lie – we can still access the private association – this is ruby remember. The important point is that we’ve made our intentions more explicit.
deferred until inspiration hits
by
Chris Roos
is licensed under a
Creative Commons Attribution 2.0 UK: England & Wales License