Ognjen Regoje bio photo

MY NAME IS
Ognjen Regoje
BUT YOU CAN CALL ME OGGY


I make things that run on the web (mostly).
More /ABOUT me.

me@ognjen.io Twitter LinkedIn Github

Functional model pattern

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

#models #orm #pattern #rails