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
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
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
project, so let’s create a class
UserWithProject. We will make it a decorator over these two models, so the class will take both
project to constructor:
1 2 3 4 5 6 7 8 9
Let’s add some actual logic to our composed model: querying for different tasks for related
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
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
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
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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
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
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
Doesn’t really look great, having separate class makes it much easier to read and understand.
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.