Blog:

Lean Models with Service Objects

Avatar
Sebastian Porto
November 29, 2013

In this post I want to describe a pattern I use a lot these days. Let me give you some background first.

While developing Rails applications it is quite common to add a lot of code into models. For example my User class might have an instance method ‘stats’. These method could do a lot of work and call many other helper methods on the way. You will often end up with models with lots of code on them.

Even worst will be the tests for that model. You will end up with a test file that covers a huge number of methods and could end up very hard to maintain.

Enter service objects

A nice way to avoid having all this logic in your model is to put your code in a different class. For example GetStatsForUserService. Then you would:

ruby serv = GetStatsForUserService.new(user) stats = serv.run

The neat thing of service objects is that your test will be very focused and easy to maintain. Now the problem is that service objects are not very discoverable, some other developer might not realise that there is a way to get stats for a user just by looking at the user class.

Wrap your service objects in an object oriented API

To deal with discoverability a simple thing to do is to just wrap the service object inside a method in the User class itself. e.g.

ruby class User def get_stats_service @get_stats_service ||= GetStatsForUserService.new(self) end def stats get_stats_service.run end end

In this way you will be able to do user.stats and get the benefits of an OO approach and service objects all together. Another benefit if that you can easy stub expensive methods in your tests. E.g.

ruby user.stub(get_stats_service).and_return(double.as_null_object)

Then you don’t have to worry about your test calling a chain of expensive processes in case something happens to call user.stats.

In Conclusion

  • Move expensive methods to service objects
  • Make focused test for those service objects In your models:
  • Wrap those service objects in class/instance methods
  • In your model test just check that the wrapper method delegates to the service object, don’t test the logic in the service object itself (this is already tested in the service object tests).
  • In most tests just stub the reference to the service object to return something expected.