I’ve previously written about the Actor model pattern I sometimes use. It allows you to split code from one model into several, each of which focuses on one user type. So, for instance, an Order
might be broken down into SupplierOrder
, BuyerOrder
and AdminOrder
. They would all operate on the orders
table but would only have the subset of functionality required for that particular user type.
The “Functional model” pattern is similar, but it focuses on the functionality required rather than user type.
Take the Order
model again as an example.
Consider an order that’s pending delivery. It needs to have access to only a subset of the functionality of the entire Order
model. So, instead of using a state machine pattern, I’d create a separate model called PendingDeliveryOrder
.
Then I’d only add the functionality required when the order is in that status to that model. For instance pending_delivery.mark_as_delivered
.
They’re implemented as normal Rails models but have an existing table name specified.
# app/models/supplier_rating.rb
class EmptyRating < ActiveRecord::Base
self.table_name = "ratings"
default_scope -> {where(score: nil)}
# rest of code applicable only for empty ratings
end
In some cases, I keep some core functionality in the base model and inherit from it:
# app/models/pending_delivery_order.rb
class PendingDeliveryOrder < Order
# In which case it's not necessary to set the table name
# rest of code applicable only for orders that
# are pending delivery
end
I also added a easy way to convert the base model into a functional one based on status:
# app/models/order.rb
def f
case status
when 'pending_payment'
becomes(PendingPaymentOrder)
when 'pending_delivery'
becomes(PendingDeliveryOrder)
else
self
end
end
# verbose version
def functionally
f
end
A nice side benefit is that you can check order status in a very readable way:
if order.functionally.is_a? PendingPaymentOrder
...
end
Benefits
Similar to Actor models, models end up smaller and easier to reason about.
It proved to be a very natural way of splitting functionality.
They nicely fit into scaffolds and resources
routes.
Potential improvements
There are, however, some improvements that I want to make, eventually.
First, I’d like the timestamps to be updated automatically. Now, I add a column like pending_delivery_updated_at
and manually set it in before_save
. I’d like to extract that functionality making it automatic. I’d also like to have pending_delivery_created_at
automatically set the first time the model is initialized.
Secondly, .last
.first
don’t work as expected because they sort based on ID. Adding the created_at
column and then sorting based on that attribute in a default_scope
would solve this.
I’d also like to be able to change the class of the model based on changes within it. For instance, if the order is marked as delivered, I’d like it’s class to automatically be changed to the following status, such as PendingReviewOrder
.
Limitations
So far, I haven’t really had issues with this pattern but I do recognize some limitations:
.f
is not very readable and only works in smaller teams, while.functionally
is too long- Functionality might be duplicated
- eg. two models can implement the same method
- However, this has been more of a feature than a bug in a few cases because it was easier to customize the functionality. eg. different
title
method depending on status
- Functionality might be inconsistent
- I understand the dangers of using a
default_scope