I recently came with the need of treating two different models alike so as to be able to query them as one ActiveRelation and also trigger their methods using a unified interface even if they are internally differently from each other.
Let’s say we have Business
and Person
models, each of them has their own database table and I want a way to retrieve all businesses and people in one single query using ActiveRecord and do it consistently returning an ActiveRelation object.
We can reach this by introducing a new model namely Entity
that will take either a business or a person as an entitable polymorphic association.
I was lucky to find that Rails 6.1 introduced the concept of delegated types the same day I needed to do this.
class Entity
delegated_type :entitable, types: %w[ Business People ]
end
And on the Business
and People
models you add:
class Business
included do
has_one :entity, as: :entitable, touch: true
end
end
Now you can do the following:
# Create a business
Entity.create entitable: Business.create(name: 'Acme' )
# Create a person
Entity.create entitable: Person.create(name: 'Martin' )
And of course you can now retrieve both businesses and people together with Entity.all
. You can even create scopes for doing some filtering.
Entity.all.businesses
Entity.all.people
This is a nice addiction to Rails. It looks great and it helps maintain the code simple and easier to read.
Now, let’s assume that every person has an associated Identity
model that returns their national identification number. We can easily delegate the method number
from Person
to the Identity
association:
class Person
belongs_to :identity
delegate :number, to: :identity, prefix: true
end
We can call person.identity_number
to retrieve the identity number directly from the person object without passing through the identity object, honoring the Law of Demeter.
The prefix: true
prefixes the method as identity_number
instead of number
as it is easier to understand that we’re calling the identity number: we want to avoid person.number
because people don’t have numbers, they have identification numbers.
Now, let’s assume a business may belong to a person.
class Business
belongs_to :person
end
If we know that entities always have an identity either directly from a person or through a business’s owner we want to be able to call the identity from the entity with the same method regardless of the type of type of entitable object.
From an entity we would want to be able do something like:
entity.person_identity_number
=> "4WA3X6E21T"
To do this, we can attempt to add delegate method in the Entity
model so Person
and Business
will receive the delegated method person_identity_number
without its prefix as identity_number
. We can try now to re-delegate from the person to the identity and from the business to the person.
class Entity
# ...
delegate :identity_number, to: :entitable, prefix: :person
end
class Person
# ...
# Wrong! We already defined :number above.
delegate :identity_number, to: :identity
end
class Business
# ...
delegate :identity_number, to: :person
end
However we quickly run into a problem: How to delegate this method down the line?
We are passing a method called identity_number
to Person
when it is already delegating the method as number
with prefix identity
(the prefix is removed when on the Identity model so the method simply becomes identity.number
.
After spending some time over this conundrum I came to the conclusion that a possible solution may look like this:
class Entity
delegated_type :entitable, types: %w[ Business People ]
delegate :identity, to: :entitable, private: true
delegate :number, to: :identity, prefix: :person_identity
end
class Person
belongs_to :identity
end
class Business
belongs_to :person
delegate :identity, to: :person
end
Now you can obtain an identity number regardless of where it is coming from a person or from a person who a company belongs to.
<%= entity.person_identity_number %>
Behind the scenes we delegated the association identity
from Entity
to :entitable
(Business or Person). If they do not handle it directly, they can pass delegate it again (as the Business
does delegate it to Person
).
You can now implement multiple models that can cuack as entitables if they either implement or delegate the identity
method.
Now, it may or it may not make sense to use delegates. I guess it depends on the complexity of the models. On one side delegate make it simple to understand the methods that are delegated on the other side it may be simpler to simple define a method to perform the delegation. This is also equivalent:
class Entity
delegated_type :entitable, types: %w[ Business People ]
def person_identity_number
entitable.identity_number
end
end
class Person
belongs_to :identity
def identity_number
identity.number
end
end
class Business
def identity_number
person.identity_number
end
end
I found it interesting to explore object composition using delegate and multi-table-inheritance as this is an interesting pattern to help keep the code simpler and easier to read.