Koz Speaks

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.

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.