An excellent talk “Blending Functional and OO Programming in Ruby” given by Piotr Solnica at Full Stack Fest 2015 made me think about improving current practices. I have tried different ways of organizing my service objects and here is what I came up with.
Background
The idea is to switch from Object Oriented mentality and stateful objects to a more functional way of doing things when only data is passed through your system.
I am working on this motivational portal. Let’s use a concept from the project
for the sake of the example. The models involved in this example are User
,
Motivation
, Shop
and Role
.
Motivation
is some data that connects Shop
where User
is working in a given month and his Role
. Each month a User
can be
assigned to different Shops
and different Roles
. Say, an employee this
month and a shop manager the next month. I call such managers “chief” since the manager was already taken.
Stuff everything into the model
For the current month we need to get the list of shops the user can manage. And to do
that we have to use multiple tables as certain roles carry chief
ability. The most obvious solution is to stuff everything into the User model.
So without further ado:
class User < ActiveRecord::Base
def chief_shops_for_month(month)
shops.merge(
motivations.for_month(month).where(role_id: Role.chief)
).uniq
end
end
Invocation is simple:
shops = user.chief_shops_for_month(month)
Hmm, that starts quite harmless. But what if we measure complexity with flog
?
10.0: flog total
10.0: flog/method average
10.0: User#chief_shops_for_month -:2
Flog
I use flog to check the complexity of my classes
and strive to keep the average complexity per method under 5
:
$ flog app
1278.9: flog total
5.0: flog/method average
29.7: User#none
23.7: TrHelper#tr app/helpers/tr_helper.rb:2
21.0: Role#none
...
Service class
To me, 10 flog per method is simply too much. We can try to break it down into two methods like so:
class User
def chief_shops_for_month(month)
shops.merge(chief_motivation_for_month(month).uniq
end
private
def chief_motivation_for_month(month)
motivations.for_month(month).where(role_id: Role.chief)
end
end
But this is ridiculous. It adds a private method on User
that is calling god
knows what. Besides I don’t like the ‘Thin Controllers, Fat Models’ concept. As
a one-man operation, there is simply not enough man-power to deal with ten feet
long models. My motto is:
Thin Everything!
I build service objects in a certain way. One public method call
with
occasional parameters passed from the controller (i.e. variables). Let’s extract
the query to what a typical service object would look like:
class User
class ChiefShopsForMonth
attr_reader :user
attr_reader :month
def initialize(user)
@user = user
end
def call(month)
@month = month
user.shops.merge(motivations_this_month_user_was_chief).uniq
end
private
def motivations_this_month_user_was_chief
user.motivations.for_month(month).where(role_id: Role.chief)
end
end
end
We keep the User
API unchanged so this is a true refactoring:
class User
def chief_shops_for_month(month)
User::ChiefShopsForMonth.new(self).call
end
end
What flog
has to say about this?
18.5: flog total
4.6: flog/method average
7.8: User::ChiefShopsForMonth#motivations_this_month_user_was_chief -:17
6.7: User::ChiefShopsForMonth#call -:10
Okay, this is better. Now it’s time to see if we can improve further and maybe make it more functional-ish.
The talk
This is the talk Piotr Solnica gave at Full-stack Fest in 2015 that got me thinking about a better way of doing things.
The gist of it: Don’t store state. Just pass everything to call
method
instead. That sounded interesting. Rolled up sleeves and here we go.
Refactoring Service object
Let’s try a full head-on solution first. I would like to throw everything in the mix:
- Dependency injection
- Command-query separation
- Keyword arguments (Smalltalk-style)
- Intention-revealing method names
CQS/CQRS Command-Query Responsibility Segregation
This is a simple principle that can be boiled down to this:
Don’t modify and request data in the same method call
Or it can be put in another way, return self
until the very last moment. That
allows chaining and storing the intermediate results and other interesting
things. The best examples in the wild I think are Rails scopes and JavaScript.
ActiveRecord Scopes can be chained up to the last moment where you can load the
relation into an array by calling to_a
on it. JavaScript is built on
returning this
. If you have ever used a JavaScript library like jQuery you
know the drill.
class User
class ChiefShopsForMonth
def initialize(role = Role, motivation = Motivation)
@role = role
@motivation = motivation
end
def call(user:, month:)
@user = user
@role = role
self
end
def shops
all_user_shops.merge(chief_this_month).uniq
end
private
attr_reader :user
attr_reader :month
attr_reader :role
attr_reader :motivation
def all_user_shops
user.shops
end
def chief_this_month
motivation.for_month(month).where(role_id: role.chief)
end
end
end
While getting here it became obvious to me that I didn’t have to use
user.motivation
as merge
is taking care of this. And I think by relying on
Motivation
class directly I can make it more intention-revealing, even if it
adds another dependency.
The new structure gains certain tidiness in the upper part. Now initialize
only
does dependency injection for the whole object. Then call
sets up the
parameters and returns self
for chaining. You don’t have to return self
,
but that gives you flexibility so I don’t see a reason not to add it. And
finally, you can get the list of shops for the given month by calling .shops
.
class User
def chief_shops_for_month(month)
User::ChiefShopsForMonth.new.call(user, month).shops
end
end
Things get hairy however after the private
mark. The state moved here
and there are four attributes instead of two. Flog is not very happy about this
either. The average complexity per method is somewhat lower, but the total is even
higher.
26.0: flog total
4.3: flog/method average
7.6: User::ChiefShopsForMonth#chief_this_month -:30
5.0: User::ChiefShopsForMonth#shops -:15
5.0: User::ChiefShopsForMonth#none
Stateless Object
One thing I would like to discuss first. As Sandi Metz says:
You can break the rules if you have a good reason or your pair lets you.
well, something along these lines at least.
So I claim that I don’t need to inject dependencies for other ActiveRecord
classes since they are not going to change. And if they will, my
service will be obsolete anyway. Thus I can get rid of initialize
. Of course, in more complex scenarios this is not the best way to go, but this is a simple query
object. So for the sake of the refactoring experiment, this will do.
I think the rule of thumb here is similar to what Piotr is suggesting:
Initialize with Service Objects you are depending on
Since I am not changing anything here, apart from the inner state of the object
and I am getting rid of the state, I don’t need a separate method that returns
the result. That allows me to shrink the object’s API to a single call
method.
This is an improvement. Since now I don’t need to pass user
and
month
separately and they can go together as parameters in a single method
call.
class User
class ChiefShopsForMonth
def call(user:, month:)
all_user_shops(user).merge(chief_this_month(month)).uniq
end
private
def all_user_shops(user)
user.shops
end
def chief_this_month(month)
Motivation.for_month(month).where(role_id: Role.chief)
end
end
end
As you can see, now the state is not stored anywhere. Okay, dependencies are not injected which is a questionable practice. But again I claim in this case this could pass.
The invocation is almost the same as in the previous example. The only difference is that we don’t have to query for shops as this is a Query Object so it only returns results straight away. And API is effectively shrunk to a single method.
Yes, you have to call new
, but only to instantiate an object. Or you can still
inject dependencies for testing. As a side effect, we can pass a hash as a parameter. Piotr suggests using a value object. I think it is overkill here. I
could easily pass the params hash, but it might hurt
readability. Now look at this:
10.4: flog total
2.6: flog/method average
5.0: User::ChiefShopsForMonth#call -:4
3.4: User::ChiefShopsForMonth#chief_this_month -:14
The whole class is almost as simple as the original single method in User
. It
encapsulates everything it needs. It doesn’t pollute models with methods that
would never be used in other places. And has an intention-revealing name and
simple single-method API.
Conclusion
These are just some ideas on how to approach the refactoring of fat models and fat controllers. Please let me know if something could be improved.
Thanks.