Encapsulation

One of the best ways to improve the design of your application is to focus on encapsulation, hiding the details of your business rules within your model classes.

One of the best ways to improve the design of your application is to focus on encapsulation, hiding the details of your business rules within your model classes.

Encapsulation, like Abstraction, has something of a bad name in the rails community. It’s seen as something practiced by UML-writing-specification-reading enterprise architects, when nothing could be further from the truth. Encapsulation is something you can, and should, do every time you sit down and code, and you don’t need a MetaFactoryFactoryVisitorImpl to achieve it.

To make my point clear, I’ll give two methods of solving the same problem. One of them nicely encapsulated, the other less so. Lets look at two different ways to implement the following functionality:

Find all the overdue customers, and send them a reminder email.

If we weren’t thinking about encapsulation, we’d probably build something like this:

1
2
3
4
5
6
7
8
9
  def remind_all_overdue_customers
    @overdue_invoices = Invoices.find(:all, 
                          :conditions=>["due_date < ? and paid = ?", 
                                        14.days.ago, false])
    @overdue_customers = @overdue_invoices.map {|i| i.customer}
    @overdue_customers.uniq.each do |customer|
      CustomerMailer.deliver_overdue_reminder(customer, Date.today)
    end
  end

But an alternative, nicer, way to implement this functionality could be:

1
2
3
4
5
  def remind_all_overdue_customers
    Customer.find_all_overdue.each do |customer|
      customer.send_payment_reminder
    end
  end

Chances are, the internal implementation of Customer.find_all_overdue and, Customer#send_payment_reminder would look a lot like what we have in the first example, but by hiding the details our code is immediately easier to read.

Another reason we encapsulate our applications is so when things change, we can localise our changes to a few select areas of the code, rather than hunting around with grep or your favourite IDE’s search functionality.

For example, if we want to give our customers 28 days to pay an invoice, we just update the code and unit tests for the find_all_overdue method on the Customer class, and we’re done. If your application looks more like the first version, chances are a small change like this will take you longer than it should.

Switching from 14 to 28 days isn’t that hard, even with the poor encapsulation. It’s really just changing some magic constant, so lets look at a harder example, adding SMS reminders for overdue accounts. Implementing this change in a well factored application is easy. The send_payment_reminder method of the Customer class just needs to check which kind of reminder to send, and act accordingly. With our first example, you have to hunt through your application for everyone using the ‘deliver_overdue_reminder’ mailer, and see if it should be checking the customers’ preferences.

The next time you find yourself implementing a feature, try this experiment I learned from Jeff DeLuca. Try to express it as a plain english sentence, if it reads well as a single ruby expression, structure your code accordingly. For our ‘reminder’ example it would be:

Send a Payment Reminder to a Customer

Which maps quite nicely to the following ruby expression.


@customer.send_payment_reminder

It’s a tiny bit more work, which will continue paying dividends in readability, testability and longevity for years to come.

Posted on October 8th, 2006 | 2 comments | Commenting Closed
Tim Haines

Tim Haines October 8th, 2006 @ 09:50 PM

Great post reinforcing the basics Koz. Nicely explained.

Testing fix of failing comments. ;-)

Henry Maddocks

Henry Maddocks October 8th, 2006 @ 10:13 PM

I’m a big fan of encapsulation. Keeping clients sticky fingers out of your privates is always good and done well it has all sorts of beneficial knock on effects like de-coupling. I have the law of Demeter tattooed on my bicep.

There is one draw back, despite what you said above, and that is testing. Not being able to get at the inner working of an object makes it hard to verify and a lot of less pragmatic people take issue with providing a testing interface.

It is also one of the contributors to the simplicity paradox wherein your system becomes simpler at the micro level but more complex at the macro level

Sponsors

Hosted excellently by RailsMachine