Using Object Models over Service Objects

This week at work I ended up having a conversation that I’ve had before about when to use service objects vs. when to use PORO model classes.

I’ve had this conversation before, a few times, and vaguely recalled that last time I was on the other side of it. So I reached out to my friend Scott hoping he could set me straight and he did. I’m going to go ahead and write it up so that hopefully next time I’m in this situation I can just refer back to here.

So, what the fuck am I talking about?

The general consensus around the office was that Service Objects were a fad that flew around the Ruby community sometime around 2014. At the time, I loved them.

Essentially, they provided a place to house “glue code” that you were going to use multiple times, usually stuff that was fairly business logic-y, or stuff that was complex and didn’t quite belong in a model or just in a controller action.

With a definition that vague, how could anything go wrong, right?

So I ended up using them a lot. I used them for things like importers. I would have a beautiful PersonImporter class that would handle things like creating a person with a given set of params. I saw the benefit because this application was creating people both in the controller and in a handful of other places like rake tasks that imported records from other sources. At this time this project also had an “evented model” where different services could talk to each other by publishing events, and some of those events might cause a Person to get created, and so it was great to have a single place that handled translating params, creating a person, validating it, creating related people records (which might involve fetching additional information), etc.

So I liked them. I thought they were a dream.

Essentially, the paradigm it had me adopting was something vaguely bigger and less defined than MVC. I had controllers, which were essentially one adapter to access my service objects (rake tasks and event handlers being two others). My models were strictly related to pulling info from the DB, validating individual record values, and defining relationships between themselves. My views were Collection+JSON serialized using the Conglomerate gem.

A little while into living in this dream world, I went to RailsConf and watched Sandi Metz’s talk about “Nothing is Something” , and like so many others I was wholly inspired to write better, more focused, more object-oriented code. If you haven’t watched that talk, seriously quit reading this and go watch that talk. You won’t even need to come back and finish this blog post because you’ll already know.

I couldn’t figure it out on my own, so I got Scott to sit down and watch the video of that talk. Here is, as far as I can remember, what we came up with.

Essentially, we were using Service Objects to hide procedural code inside our Object Oriented design. Mostly to avoid coming up with the correct nouns. Fucking naming things, right?

I didn’t know how to name an object that imported things outside of verbing it, so I just verbed it and threw it in app/services. Which, like, totally fine. It’s a cop-out, but, seriously fuck naming things.

The problem is that it encourages you to write less object-oriented, more procedural style code.  I had a lot of code that looked like this:

https://gist.github.com/jakewilkins/92816efbae675cc3a739583a5703cef0

Which is at least organized. It’s not great, but it’s pretty easy to see how I got here. It’s stuffing the complexity further and further down, hopefully creating a top level that’s straightforward and easy to follow.

The thing is, it would be fairly trivial to take this and make it more traditionally OO.

If we just name it correctly, this same thing can happen and be nicely wrapped in much more familiar OO mindset.

Essentially, my service objects were badly formed wrappers around an object that represented some sort of ExternalModel that I didn’t have named in my app.

To name these models better, let’s have a for instance that I’m important people from IMDB using my PersonImporter.

I could instead have a Imdb::Person, living inside of app/models/imdb/person.rb. In IMDB, people have multiple movies, and I would want to suck those down to. So I could have a Imdb::Movie model stored similarly. When a Imdb::Person needs a movie, it creates and instance of a Imdb::Movie, or vice versa.

Once we have our objects setup, sending the familiar #save message would handle translating those external models into their equivalent internal counterparts.

The benefits here seem kind of small. We definitely haven’t solved all the worlds problems.

But I think there is absolutely a benefit here. We’ve avoided introducing a poorly defined abstraction that we have to deal with for the lifetime of our app. Having that model named correctly clearly defines what it represents. It should be clear to anyone looking that a Imdb::Person represents a person, who has something to do with IMDB. I go back and forth in my head whether #save should be #import. If I figure it out I’ll try to come back to add it here so I don’t forget again.

I think for me, service objects were a necessary stepping stone to get from spaghetti everywhere to something more OO. They did a fine job of centrally locating logic that would otherwise have been spread around my code, some in controllers, some in models, and all leaking bugs.

But ultimately I hope next time I remember a little quicker that naming things correctly is always a good idea and in the end leads to cleaner, clearer abstraction layers.

974 Words