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 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.