Rails - Law of Demeter and delegate

Subscribe to receive new articles. No spam. Quality content.

Today's post will be more about Ruby On Rails and ActiveRecord. But first things first. If you didn't hear about Law of Demeter, I really recommend to read about it.

In general - it's a set of rules which bounds knowledge of modules about each other and allows you to reduce coupling of your system.

Here is how Wikipedia describes it:

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.

The main idea that objects should know as little as possible about structure and properties of any other component of a system.

This description allows us to understand the concept. Having this knowledge, let's get back to ActiveRecord and relations between models.

Here is a code that you might see very often:

class User < ActiveRecord::Base
  has_one :address
  has_many :orders
end

class Address < ActiveRecord::Base
  belongs_to :user
end

class Order < ActiveRecord::Base
  belongs_to :user
end

Nothing new here: User has one Address and has many Orders. Address and Order belong to User.

With this data structure, people often do something like that in a view layer:

<%=order.user.name %>
<%=order.user.address.street %>
<%=order.user.address.city %>
<%=order.user.address.country %>

By this code we violate Law of Demeter. Because ActiveRecord allows us to work with associations easily - usually we don't care enough about coupling between objects.

In this case, we got knowledge about User through Order. We've got user's address data from it. Order knows too much about user.

If we don't want to break Law of Demeter - we shouldn't use more than one "." in our code. It's a simplification, but still does the trick.

For example: order.user_name is a good code, because it's ok for order to know name of user.

So we could rewrite that code in the following way:

class User < ActiveRecord::Base

  has_one :address
  has_many :orders

  def street
    address.street
  end

  def city
    address.city
  end

  def country
    address.country
  end

end

class Order < ActiveRecord::Base

  belongs_to :user

  def user_name
    user.name
  end

  def user_street
    user.street
  end

  def user_city
    user.city
  end

  def user_country
    user.country
  end

end

Having these changes, we can rewrite code in view layer:

<%=order.user_name %>
<%=order.user_street %>
<%=order.user_city %>
<%=order.user_country %>

Let's think about benefits of this approach.

Good parts: we don't cross boundaries of two objects explicitly using associations (order.user...). We do that in more abstract way, using wrapper for those attributes.

Those wrappers at the same time creates a code smell. Because every time we want to add one more attribute we have to do that in two places. It's hard to maintain such code.

In this case Rails helps us, providing a delegate.

It's easy to understand how it works by looking at simple example:

class User < ActiveRecord::Base

  has_one :address
  has_many :orders

  delegate :street, :city, :country, to: :address

end

class Order < ActiveRecord::Base

  belongs_to :user

  delegate :name, :street, :city, :country, to: :user, prefix: true

end

Looks much better, right?

Let's check how delegate works for User model: It passes all calls of methods :street, :city, :country to object address which we have through has_one association. So if we call current_user.street - it will be translated to current_user.address.street since we delegated street call to address

In case with Order we have the same approach, but with a small addition, we pass parameter: prefix: true. It means that it will add prefix _user to all methods we delegated: user_name, user_street, user_city, user_country.

With these changes order.user_name will call order.user.name. order.user_street calls order.user.street, which delegated this method to user.address.street.

By delegating methods to other objects we don't break Law of Demeter and don't create useless getter methods for attributes.

Try to use this approach next time if you see such chain of calls: order.user.address.street. So you should get something like this: order.user_street.

PS: If you work with pure Ruby (without Rails) but still want to use this approach, you can use Forwardable which allows to delegate methods as well.

Subscribe to receive new articles. No spam. Quality content.

Comments