Karol Galanciak - Ruby on Rails and Ember.js consultant

Model Your Domain With Composed Models

In many Rails applications the modeling is limited only to creating classes inheritng from ActiveRecord::Base which are 1:1 mapped to database tables. Having AR models like User and Project doesn’t tell much about the domain of your application. Well, you can add some domain logic to the models but that way you will easily end up having giant classes with thousands lines of code, so let’s just use models for database-related stuff only. Other option would be to create service objects for every usecase. That’s a good and clean way, but it’s merely the interaction layer between different parts of your application. You may end up easily with all logic encapsulated within service objects that will start looking more like procedural programming rather than proper OOP and not real domain API at all. The good news is that you can easily counteract it: time to use composed models.

Definition and examples composed models

Composed models are classes which emphasize the relation between entities and the interactions between them. In most applications the models have multiple different relations between each other, rarely are they self-contained. So what happens if we take some entities and try to put them in one class?

Imagine you are developing project management application and you’ve got User, Project and Task models. What are the possible interactions between these models? User can be assigned to many tasks in a given project, so we can both query for the existing tasks and add some new tasks. We would probably query for finished tasks, currently being done and the ones not started. We may also check if user is in given project or can add/remove him/her from the project. In this case, the predominant relation is the one between the user and project, so let’s create a class UserWithProject. We will make it a decorator over these two models, so the class will take both user and project to constructor:

1
2
3
4
5
6
7
8
9
class UserWithProject
  attr_reader :user, :project
  private     :user, :project

  def initialize(user, project)
    @user = user
    @project = project
  end
end

Let’s add some actual logic to our composed model: querying for different tasks for related user and project, adding new tasks, checking if user is assigned to the project and maybe leaving the project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class UserWithProject
  attr_reader :user, :project
  private     :user, :project

  def initialize(user, project)
    @user = user
    @project = project
  end

  def tasks
    @tasks ||= project.tasks.with_assignee(user)
  end

  def pending_tasks
    tasks.pending
  end

  def finished_tasks
    tasks.finished
  end

  def not_started_tasks
    tasks.not_started
  end

  def add_task(task)
    task.asignee = user
    task.project = project
    task.save!
  end

  def assigned_to_project?
    project.users.include?(user)
  end

  def leave_project
    project.users.destroy(user)
  end
end

Most methods are probably self-explanatory and don’t need to be discussed. Now we have an actual domain model which neatly encapsulates interactions between users, projects and tasks. Looks like a real API for application that can be simply (re)used.

One usecase for composed models is about interactions between models. But this patterns also shines when you consider some modifiers of values. By modifier I mean an object that has some kind of influence on values being returned by methods of other object. For example you might be developing an e-commerce app and have Order with total_price. Let’s imagine that you need to handle discounts for orders, which as you max expect, are going to decrease total_price. With composed model pattern you could create OrderWithDiscount class. To make it still behave like an Order instance, the class may inherit from SimpleDelegator and all the method calls not implemented by OrderWithDiscount are going to be delegated to Order:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OrderWithDiscount < SimpleDelegator
  attr_reader :order, :discount
  private     :order, :discount

  def initialize(order, discount)
    @order = order
    @discount = discount
    super(order)
  end

  def total_price
    # somehow apply the discount.value to order.total_price
  end
end

That way you can still have total_price on Order without adding additional arguments, conditionals etc. for handling discounts and have a separate object for special usecases.

Extracting existing logic to composed models

Now that you know how what are the composed models for, you may be wondering how to extract already existing codebase to that pattern. Fortunately, it’s easy to tell in many cases if a particular method is a good fit to move. When you have multiple methods in one class taking the same kind of argument(s), that’s probably a good idea to think about some changes. Let’s use the example with User and Project. If that pattern hadn’t been used there we would probably have had some code in Project model looking like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Project < ActiveRecord::Base
  # associations and stuff like that

  def tasks_for_user(user)
    # some logic
  end

  def pending_tasks_for_user(user)
    # some logic
  end

  def finished_tasks_for_user(user)
    # some logic
  end

  def add_task_for_user(task, user)
    # some logic
  end
end

This class really begs for refactoring ;). Another sign would be having methods where there might be an argument modyfing the value or may not. Using the Order example, there could be a method like:

1
2
3
4
5
6
7
8
class Order < ActiveRecord::Base
  def total_price(discount: nil)
    # somehow calculate the total_price
    if discount?
      # apply the discount
    end
  end
end

Doesn’t really look great, having separate class makes it much easier to read and understand.

Wrapping up

I’ve shown a pretty cool pattern I’ve started using recently, which works really great and makes a big difference when looking at the domain logic of the application. I hope you will find it useful.

Comments