I recently stumbled across Jason Swett’s “Beware of ‘service objects’ in Rails” blog post. While I appreciate the perspective, I think it gets a number of things wrong. I replied to this post in a comment, but wanted to post my thoughts here as well in the hopes of encouraging more dialog about this important topic.
If so inclined, please feel free to comment below with your take on this subject.
(NOTE: The following refers heavily to the example Tweet
class in the original blog post, under the heading “A better alternative to service objects: domain objects“, which you may want to review before continuing.)
Thanks for the thoughts. However, I have to disagree. Here are a few counterpoints:
- While
Tweet.send
and the entireTweet
class may look very object-oriented, it in fact violates many OOP principles (single responsibility, separation of concerns, etc.). Additionally, theTweet
class is a field day. Anyone can and will add methods with anything that they think a Tweet can do, including CRUD operations, searching the DB for tweets, searching the Twitter API for tweets, etc. Generic, noun-based names encourage such misuses/abuses. - Good service classes don’t use [generic] names like “Manager” or “Service”. Instead, it’s clear from their name what service they provide, IOW what they do:
TweetRepository
,TwitterApiClient
, etc. - Service objects are object oriented when they act on interfaces instead of implementations. So, a hypothetical
TweetClient
might look like:
class TweetClient
def initialize
@api_client = TwitterApiClient.new
@validator = TweetValidator.new
end
def send(tweet)
return unless @validator.valid?(tweet)
@api_client.post(
tweet.as_json,
TwitterApiClient::TWEET_CREATE_URL
)
end
end
angry_rant = Tweet.new('Hello world!')
TweetClient.new.send(angry_rant)
The above service class is 100% object oriented while also clearly delineating the responsibility of each class. Also, object composition is better than inheritance for reducing coupling in an app.
One other thing to note that I didn’t include in my original response:
In a standard Rails app, if the Tweet
class lives in the app/models/
folder then it probably inherits (either directly or indirectly) from ApplicationRecord
. Thus, it’s associated with the M portion of an MVC application and should be concerned with database CRUD operations, so adding a send
method that posts to an external API muddies the water considerably regarding the responsibility of this class, makes testing much more complicated, and is a patent violation of many software development best practices.
Therefore, when adding new functionality to a class that inherits from a parent class, we would do well to consider if said new functionality makes sense in context of the entire inheritance chain. If it doesn’t, then that’s a good indication that we probably need to add that functionality elsewhere, and a service class would be a good candidate.
Just my $0.02. Thanks for reading!