Encapsulation in Active Record objects
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 (has_one, has_many, belongs_to and has_and_belongs_tomany) 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.