The good and bad thing about Active Record models
In the early stages of a Rails project’s life, the pattern of putting all the model code into objects that inherit from Active Record works out pretty nicely. Each model object gets its own database table, some validations, some associations, and a few custom methods.
Later in the project’s lifecycle, this pattern of putting everything into Active Record objects gets less good. If there’s an
Appointment model, for example, everything remotely related to an appointment gets put into the
Appointment model, leading to models with tens of methods and hundreds if not thousands of lines of code.
Despite the fact that this style of coding—stuffing huge amounts of code into Active Record models—leads to a mess, many Rails projects are built this way. My guess is that the reason this happens is that developers see the organizational structures Rails provides (models, controllers, views, helpers, etc.) and don’t realize that they’re not limited to ONLY these structures. I myself for a long time didn’t realize that I wasn’t limited to only those structures.
Service objects as an alternative to the “Active Record grab bag” anti-pattern
A tactic I frequently hear recommended as an antidote to the “Active Record grab bag” anti-pattern is to use “service objects”. I put the term “service objects” in quotes because it seems to mean different things to different people.
For my purposes I’ll use the definition that I’ve been able to synthesize from several of the top posts I found when I googled for
rails service objects.
The idea is this: instead of putting domain logic in Active Record objects, you put domain logic in service objects which live in a separate place in your Rails application, perhaps in
app/services. Some example service class names I found in various service object posts I found online include:
So, typically, a service object is responsible for carrying out some action. A
TweetCreator creates a tweet, a
RegisterUser object registers a user. This seems to be the most commonly held (or at least commonly written about) conception of a service object. It’s also apparently the idea behind the Interactor gem.
Why service objects are a bad idea
One of the great benefits of object-oriented programming is that we can bestow objects with a mix of behavior and data to give the objects powerful capabilities. Not only this, but we can map objects fairly neatly with concepts in the domain model in which we’re working, making the code more easily understandable by humans.
Service objects throw out the fundamental advantages of object-oriented programming.
Instead of having behavior and data neatly encapsulated in easy-to-understand objects with names like
User, we have conceptually dubious ideas like
RegisterUser. “Objects” like this aren’t abstractions of concepts in the domain model. They’re chunks of procedural code masquerading as object-oriented code.
A better alternative to service objects: domain objects
Let me take a couple service object examples I’ve found online and rework them into something better.
The first example I’ll use is
TweetCreator from this TopTal article, the first result when I google for
rails service objects.
class TweetCreator def initialize(message) @message = message end def send_tweet client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
I think it’s far better just to have a
class Tweet def initialize(message) @message = message end def deliver client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
Isn’t it more natural to say
Tweet.new('hello').deliver than to say
TweetCreator.new('hi').send_tweet? I think so. Rather than being this weird single-purpose procedure-carrier-outer,
Tweet is just a simple representation of a real domain concept. This, to me, is what domain objects are all about.
The differences between the good and bad examples in this case are pretty small, so let me address the next example in the TopTal article which I think is worse.
Currency exchange example
module MoneyManager # exchange currency from one amount to another class CurrencyExchanger < ApplicationService ... def call ActiveRecord::Base.transaction do # transfer the original currency to the exchange's account outgoing_tx = CurrencyTransferrer.call( from: the_user_account, to: the_exchange_account, amount: the_amount, currency: original_currency ) # get the exchange rate rate = ExchangeRateGetter.call( from: original_currency, to: new_currency ) # transfer the new currency back to the user's account incoming_tx = CurrencyTransferrer.call( from: the_exchange_account, to: the_user_account, amount: the_amount * rate, currency: new_currency ) # record the exchange happening ExchangeRecorder.call( outgoing_tx: outgoing_tx, incoming_tx: incoming_tx ) end end end # record the transfer of money from one account to another in money_accounts class CurrencyTransferrer < ApplicationService ... end # record an exchange event in the money_exchanges table class ExchangeRecorder < ApplicationService ... end # get the exchange rate from an API class ExchangeRateGetter < ApplicationService ... end end
First, the abstractions of
CurrencyExchanger, etc. aren’t really abstractions. I’m automatically skeptical of any object whose name ends in -er.
I’m not going to try to rework this example line for line because there’s too much there, but let’s see if we can start toward something better.
class CurrencyValue def initialize(amount_cents, currency_type) @amount_cents = amount_cents @currency_type = currency_type end def converted_to(other_currency_type) exchange_rate = ExchangeRate.find(@currency_type, other_currency_type) CurrencyValue.new(@amount_cents * exchange_rate, other_currency_type) end end one_dollar = CurrencyValue.new(100, CurrencyType.find('USD')) puts one_dollar.converted_to(CurrencyType.find('GBP')) # 0.80
Someone could probably legitimately find fault with the details of my currency conversion logic (an area with which I have no experience) but hopefully the conceptual superiority of my approach over the
MoneyManager approach is clear. A currency value is clearly a real thing in the real world, and so is a currency type and so is an exchange rate. Things like
ExchangeRateGetter are clearly just contrived. These latter objects (which again are really just collections or procedural code) would probably fall under the category of what Martin Fowler calls an anemic domain model.
Suggestions for further reading
Enough With the Service Objects Already
I really enjoyed and agreed with Avdi Grimm’s Enough With the Service Objects Already. Avdi’s post helped me realize that most service objects are just wrappers for chunks of procedural code. What I wanted to add was that I think it’s usually possible to refactor that procedural code into meaningful objects. For example, in a piece of schedule code I recently wrote, I have a concept of an
AvailabilityBlock and a mechanism for detecting conflicts between them. Instead of taking the maybe “obvious” route of creating a
AvailabilityBlockConflictDetector, I created an object called an
AvailabilityBlockPair which can be used like this:
AvailabilityBlockPair.new(a, b).conflict?. To me this is much nicer. The concept of an
AvailabilityBlockPair isn’t something that obviously exists in the domain, but it does exist in the domain if I consider it to be. It’s like drawing an arbitrary border around letters in a word search. Any word you can find on the page is really there if you can circle it.
Anemic Domain Model
Martin Fowler’s Anemic Domain Model post helped me articulate exactly what’s wrong with service objects, which seem to me to be a specific kind of Anemic Domain Model. My favorite passage from the article is: “The fundamental horror of this anti-pattern [Anemic Domain Model] is that it’s so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What’s worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.”
Martin Fowler on Service Objects via the Ruby Rogues Parley mailing list
This GitHub Gist contains what’s supposedly an excerpt from an email Martin Fowler wrote. This email snippet helped clue me into the apparent fact that what most people call service objects are really just an implementation of the Command pattern. It seems to me that the Interactor gem is also an implementation of the Command pattern. I could see the Command pattern making sense in certain scenarios but I think a) when the Command pattern is used it should be called a Command and not a service object, and b) it’s not a good go-to pattern for all behavior in an application. I’ve seen engineering teams try to switch over big parts of their application to Interactors, thinking it’s a great default style of coding. I’m highly skeptical that this is the case. I want to write object-oriented code in Ruby, not procedural code in Interactor-script.
Don’t Create Objects That End With -ER
If an object ends in “-er” (e.g.
Processor), chances are it’s not a valid abstraction. There’s probably a much more fitting domain object in there (or aggregate of domain objects) if you think hard enough.
The Devise gem code
I think the Devise gem does a pretty good job of finding non-obvious domain concepts and turning them into sensible domain objects. I’ve found it profitable to study the code in that gem.
I’m slowly trudging through this book at home as I write this post. I find the book a little boring and hard to get through but I’ve found the effort to be worth it.
I find the last sentence of Martin Fowler’s Anemic Domain Model article to be a great summary of what I’m trying to convey: “In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you’ve robbed yourself blind.”
Don’t use service objects. Use domain objects instead.