How I code without service objects

by Jason Swett,

What service objects are

Service objects and Interactors are fashionable among Rails developers today. I can’t speak for everyone’s motives but the impression I get is that service objects are seen as a way to avoid the “fat models” you’re left with if you follow the “skinny controllers, fat models” guideline. If your models are too fat you can move some of their bulk to some service objects to thin them out.

Unfortunately it’s not quite possible to say what service objects are because the term “service object” means different things to different people. To some developers, “service object” is just a synonym for plain old Ruby object (PORO). To other developers, a service object is more or less an implementation of the command pattern. Interactors are similarly close to the command pattern.

For the purposes of this post we’ll go with the “command pattern” conception of a service object: a Ruby class with a single method, call, which carries out a sequence of steps.

I have some gripes with the way service objects are advocated in the Ruby community. I’ll explain why.

Pattern vs. paradigm

The command pattern has its time and place. Sometimes it makes plenty of sense to bundle up an action into a single object and let that object orchestrate the steps that need to happen for that action.

But the command pattern is a pattern, not a paradigm. A software design pattern is a specific solution meant to meet a specific problem. A programming paradigm (like object-oriented programming or functional programming) is a style that can be applied at the level of entire applications.

What pains me a little bit to see is that many developers seem to have confused the idea of a pattern with the idea of a paradigm. They see service objects or Interactors and think, “This is how we’ll code now!” They have a service-object-shaped hammer and now everything is a nail.

Imperative vs. declarative

An imperative programming style expresses code mainly in terms of what to do. A declarative programming style expresses code mainly in terms of what things are.

For those not very familiar with imperative vs. declarative, here’s an explanation by way of example.

An imperative sandwich

If I were to tell a robot to make me a peanut butter and jelly sandwich using the imperative style, I would say, “Hey robot, get two pieces of bread, put some peanut butter on one, put some jelly on the other, put them together, put it on a plate, and give it to me.”

A declarative sandwich

If I were to do the same thing in a declarative style, I would say, “A sandwich is a food between two pieces of bread. It comes on a plate. A peanut butter and jelly sandwich is a sandwich that contains peanut butter and jelly. Make me one.”

My preference for declarative

I’m strongly of the view that declarative code is almost always easier to understand than imperative code. I think for the human brain it’s much easier to comprehend a static thing than a fleeting action.

At some point the things do have to take action (the peanut butter and jelly sandwich has to get made) but we can save the action part for the very highest level, the tip of the pyramid. Everything below can be declarative.

Service objects are all imperative

Service objects and Interactors are basically all imperative. Their names tend to be verbs, like NotifySlack or AuthenticateUser. Their guts tend to be imperative as well. To me this is a pity because, like I said above, I think most things are more clearly expressed in a declarative style. Even things that seem at first glance to lend themselves more naturally to an imperative style can often be expressed even more naturally in a declarative way.

Takeaways

  • The command pattern is a pattern, not a paradigm. It’s not a style, like OOP or FP, that should be applied to all your code.
  • Declarative code is often easier to understand than imperative code.
  • Regular old OOP is a fine way to program. Not everything in Rails has to utilize a fancy tool or concept.

15 thoughts on “How I code without service objects

  1. Peter Schröder

    I like that you are explaining your style through a more nuanced, real world example that is still reasonably easy to follow.

    The way you are adressing the topic is pretty much how I’ve made up my mind about this topic.

    Another thing that really botheres me with the `.call` pattern in the case of interactors is the hassle with discoverability. If all your methods are just named `.call` it’s hard to search for locally, in github, in your exception tracker…

    One thing that I’d like to comment on is your `ChargeCollection` class. I understand the desire to add functionality to collections, but I generally refute when someone extends from base-classes like Array, Hash etc. It’s usually more appropriate to put this logic elsewhere. I think the only exception to this is when you are building a framework where all objects are interacting with a same set of base-classes-on-steroids.

    Reply
  2. Simon

    Great article!
    I agree that sometimes service objects get a little abused.
    However, in most of the cases they are used as part of the CRUD set of actions, where the majority of the logic revolves around building the object to be stored in the DB + maybe sending some notifications etc.
    How would you model something like that with this OOP approach?

    Also, in your example, who would call the initial #write method? The controller?

    One final thing, I’m not sure that app/models is the right place to put this kind of code. app/models is usually reserved for Active Record models. I would expect your code to be under /lib instead.

    Reply
    1. Jason Swett Post author

      Thanks! I’ll answer your questions in reverse order. Regarding where to put things, I put all my domain logic in app/models and all my application logic in lib. The #write method happens to get called just by another model in this case. Regarding how I would model something like that in OOP, I’m not sure. Maybe I wouldn’t. I’d have to understand more about the scenario in order to know how I would code it.

      Reply
  3. Todd

    Jason – Thanks for sharing this article. I definitely agree with you that an OOP design often is a better fit and helps with creating more understandable/manageable code.

    Have you considered combining OOP design with Service Objects/Interactors? Doesn’t really fit the more common definitions of Service Objects but that is what I’ve been doing for a while.

    The basic structure still includes a `call` class-level method but what the `call` method does is where things tend to diverge. For me, the `call` method is just a simple way to create an instance of the Service Object, run it by calling an instance method (I use `go` but could be anything), and return the created instance.

    There are many reasons for this Service Object design but it primarily came out of trying to figure out what a Service Object should return. I’ve always thought that suggestions for a Service Object return value were either too complex (creating a special return object) or too opaque (no return value, check the database to make sure it worked).

    Returning the Service Object instance allows for an OOP design on top of the Service Object pattern. Then you can design the Service Object so that it’s easy to inspect what the Service Object did (ex: new objects created or updated). In the case of report generation, like in your example, you would have public methods for retrieving the part of the report that the Service Object is responsible for.

    Perhaps keeping the Service Object pattern isn’t that important to you but it wouldn’t change too much of your code. You would keep most of the same classes, add a few standard methods to each object, and maybe do some minor restructuring. I also like to name my Service Objects as verbs (`LineSerializer` -> `SerializeLine`) to keep me in the right frame of mind when writing the code but I don’t see that as important as the basic structure.

    A couple of other things I think about when designing this type of code is writing automated tests and background jobs. The OOP Service Object works well for those situations too.

    Reply
  4. Augusts

    Thanks for writing this up Jason. I must say I have contracted a bit of a service-object-itis in that lately almost everything I’ve written, that is not a model or a controller, has been a service.

    My justification for that is that web applications lend themselves to the (over)use of command pattern well. Action code works well by making a command call and rendering the return value. Job code works the same way because return value is irrelevant. Ditto for callbacks. Perhaps the only place for objects with several methods in Rails’ MVC is the missing “view” object.

    From the perspective of a service-object-junky

    ZirMed837PFile::File.new(appointments).write(file.path)

    looks hard to test (will have to assert/mock instantiation and call to write), and requires the developer to remember both how to instantiate and then what to call on the instance. The API can be KISSed down to one method call.

    A service call enthusiast would say that for the purposes of writing the ZirMed837PFile, the calling object does not care, indeed ideally should not have access to, any writing or aggregating instances. Merely a writer service that, when called with appropriate params performs the needed operation, some

    ZirMed837PFileWriter.call(
    appointments: appointments,
    path: file.path
    )

    This is simple to contract-test – assert that writer was called with correct params, mock returned value, if any, and assert that the value was used correctly.

    Perhaps this is dumbing Ruby down to BASIC and losing some OOP magic, but I find that when working in a team the understanding that everyone writes services that have just one public method and only use kwargs to be liberating. Understanding another’s service generally only requires reading the “.call” spec.

    Reply
  5. Alex

    Thanks for the article! It’s always nice to have a look on well structured code. But there is a one point in my mind about this approach vs .call . The deal is you approach is basically a freedom, it fits to experienced programmers who knows how to decompose their code and not mess it up and the .call approach pushes less experienced developers to more simple and narrow interface so their code looks similar so new developers can stick to it and save good structure instead of practising in decomposing task to plain objects which is not simple at all for less experienced developers. Anyway I think both approaches can be used in the same project with good balance.

    Reply
    1. Jason Swett Post author

      Thanks! I would argue that if the .call approach results in a more simple and narrow interface, and if a more simple and narrow interface is easier to understand, then everyone should do it that way, not just less experienced developers. But I don’t think using .call does result in more understandable code. I think using .call gives the illusion of simplicity and organization without genuinely providing simplicity and organization. What I would prefer is that less experienced developers are taught good OOP practices.

      Reply
  6. Ajay

    Interesting and thought-provoking.

    What do you think about “SerializedLine” instead of “LineSerializer”, to preserve the declarative naming convention and avoiding “er”.

    Reply
  7. Denys

    Thank you, interesting opinion.But I think those classes shouldn’t be in models, it rather lib folder, since you could re-use it in other projects, or even extract it to gem. Models just for AR, plain objects there are confusing.

    Reply

Leave a Reply

Your email address will not be published.