Rails - Law of Demeter and delegate
- 10 September 2016
- Ruby Ruby On Rails
- Rails - Law of Demeter and delegate
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
Order belong to
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
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.
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
It passes all calls of methods
:street, :city, :country to object
address which we have through
So if we call
current_user.street - it will be translated to
current_user.address.street since we delegated
street call to
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.street, which delegated this method to
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:
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.