Recently I've been considering Service Objects, in particular how they might be named and whether they should have class methods or not.
There seem to be two popular approaches to naming, which are well defined in the posts linked to in the following sections, with some of my own thoughts on these approaches.
Adapting the example given in Dave Copeland's post Anatomy of a Rails Service Object, say we have a thing that we want to process. We might create a service object called
ThingProcessor and invoke it like this:
result = ThingProcessor.new.process(thing)
This might lead to something like:
result = WelcomeEmailSender.new.send(current_user)
The approach described by Philippe Creux in Gourmet Service Objects is to place the verb first, followed by the thing noun, hence
ProcessThing. We might call this as follows:
result = ProcessThing.call(thing)
call is a convenience class method that first instantiates
ProcessThing then invokes the
.call method on that instance.
This might lead to something like:
result = SendWelcomeEmail.call(current_user)
Which is better?
As with so many things, it may just come down to a matter of style; but here are some of my thoughts on it.
Service objects should aim to be as functional as possible, and therefore carry minimal (or preferably no) state. They should also be instantiated only for as long as a single 'invocation' of the service they provide. For me, the convention
ThingProcessor implies a more persistent object that might be passed into some other process to handle a particular concern. Such as a
xxxHandler. I also find the convention too similar to the conventions you might use in, say, the Strategy pattern.
Conversely, it might seem odd to start a class name with a verb, as in
SendEmail. However, in the case of a service object, I find this convention more expressive. Rather than implying an instance of the class is 'something that sends emails', it encourages us to interpret an instance of the class is 'the sending of an email'. It's a subtle difference, but nevertheless more accurate.
Invoking the service
Typically, almost all methods of the service object should be private, and the class should have one public entry point; but what to call it? (pun intended)
result = ThingProcessor.new.call(thing) # or result = ProcessThing.new.call(thing)
Arguably, in this case, the first example is marginally clearer. We can improve both of these approaches though by introducing a class helper method to take care of the service object instantiation:
result = ThingProcessor.call(thing) # or result = ProcessThing.call(thing)
Now it's about even. But having a method
call() which is invoked on a function object, rather than what we have here, which is a class.
Introducing a class method as above is also discussed in Mike Burns's post Meditations on a Class Method. Here, he uses the method
.run. One argument in favour of
.run is that it's reminiscent of the Command pattern. While this is true, in the Command pattern the process invoking
.run necessarily doesn't know what the command does only that it provides that method. A Service Object is different; we invoke it explicitly because we know what it does. Therefore, I feel we can afford to be a bit more expressive...
In the case where the service object takes one value object as an argument, the method name can be very expressive indeed:
result = WelcomeEmailSender.sendTo(current_user) # or result = SendWelcomeEmail.to(current_user)
Here, the second approach is definitely more satisfying.
.to can always alias
.run for those who want to provide that interface as well.
However, although I'm leaning towards the second approach, it can become tricky when it's necessary to pass the
thing as a parameter or when there are multiple parameters. i.e.:
# this doesn't feel quite right now: result = SendEmail.to(current_user, email) # although this is quite amusing, it's still not right: result = PrintReport.print(report)
There are other method names we might consider, such as
result = SendEmail.for(email, user) # or result = PrintReport.do(report) # or result = ProcessThing.with(thing)
do are Ruby keywords so probably better avoided. I rather like
.with and this becomes even more expressive when we switch to using keyword arguments:
# not as satisfying as SendWelcomeEmail.to(current_user), but not far off: result = SendWelcomeEmail.with(user: current_user) # and with the other examples: result = SendEmail.with(email: email, user: current_user) result = PrintReport.with(report: report) result = ProcessThing.with(thing: thing)
In conclusion then, I think my preferred approach to naming service objects is going to be
VerbObject.with(keword: argument). The only time this doesn't really work is if there are no arguments to the service object at all. But then I'm struggling to think of when this would ever happen...