← Back to Upcase

Naming service classes


(Ralph Wintle) #1

Hi everyone,

You’ll probably see this type of post on every Ruby forum around but I thought I’d bring it up to see what Thoughtbot says :smile:

I’ve had this discussion before with my mentor @georgebrock and wanted to get everyone else’s opinion. I’m trying to formulate my own convention around naming service classes based on what Thoughtbotter’s prefer to do.

So how do you name/create your service objects?

Using .call?

class AcceptInvite
  def self.call(invite, user)
    invite.accept!(user)
    UserMailer.invite_accepted(invite).deliver
  end
end

I’m not such a fan of this approach as I don’t think .call reads well and I don’t tend to inject behaviour via lambdas which this is meant to facilitate. (More on this approach here: http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html)

Using a class method?

UserSubscription.register(input)

Where you create a class method which delegates to .new

def self.register(input)
    new(input).register_subscription
end

Not using a class method?

UserSubscription.new(input).register

And then in respect to the larger issue at hand, what naming conventions do you use? Verbs, nouns?

I know quite a few people are against using verbs like UserCreator. I’m not a fan because you just end up with syntactic vinegar such as UserCreator.call or UserCreator.create.

Using just nouns is cool, but it’s not always apparent what you should name things i.e. RegistrarInviteAcceptor?

The last issue I often see is where service objects often overlap with other types of objects that may use the command pattern or factory pattern? What do you name those types of objects? Do you append Command or Factory to them?

I know everyone has their own style of doing this so I’d like to hear how you approach this? My goal is to figure out my own personal convention so I can create some consistency with my service objects.


(Joel Oliveira) #2

Hey @ralphwintle. For our team we’ve picked a convention and stuck with it. That’s the first and most concrete “rule” I would stand fast on. Pick one and go with it - whether it’s call or go or run … no matter, in my opinion. They work.

We’ve chosen to go with the NounVerber.run OR NounVerber.new(noun_obj).run convention. Eg: UserCreater.run or RoleAssigner.new(@user, :admin).run. It works fine. People know where to find them (./app/commands) and what the contract is.

The other item you touch upon is the class method delegating to an instance of self. I’d follow this rule as well. Read @mikeburns take on this here: http://robots.thoughtbot.com/meditations-on-a-class-method … if you haven’t yet, it’s a really great read.

I don’t remember what the exact convention was within thoughtbot - I seem to remember things like this varying between projects, depending on the team.

Hope this helps.


(Ben Orenstein) #3

Joel’s right, we don’t have a set convention for this.

Personally, I tend to use Joel’s approach.


(Ralph Wintle) #4

Thanks @joel.

After reading @mikeburns article again and giving it some thought, the benefit of this approach appears to be in the case where you might use this class as a collaborator?

My thinking was that you could just inject it as a test double that would respond to the #run method instead of passing in a test double that stubs out #new and returns another test double that responds to #run. (I tried to think of an example to show but it’s really hard!)

That approach makes a lot of sense to me now so I think I’ll try it out. Perhaps I’ll give the NounVerber.run(non_obj) naming convention a go too.

Thanks @benorenstein for the input. I was looking through the Upcase repo to try pick out any patterns but this does seem to vary quite a bit.

In one of the projects I work on I can sometimes see 2/3 different implementations when creating service objects, which kind of irks me a bit, since I feel like there should be some consistency there but I guess that’s more of a higher level issue with regard to setting out the"agreed approach for this project" from the start.


(Andy Waite) #5

I prefer to use nouns for all class names. I think UserCreator reads much better than CreatesUser. I think that style of naming makes more sense for a module or concern which is mixed into a class.

I usually name the calling method run or invoke. I generally avoid echoing the class name, e.g. noUserCreator.create.

I use the same approach as @mikeburns where the class method just instantiates an instance. This make it easier in tests to verify that a collaborator receives the expected message. So, for example, in an isolated controller spec instead of having to write:

user_creator = double
allow(UserCreator).to receive(:new).and_return(user_creator)

post :create, name: "Andy"

expect(user_creator).to have_received(:invoke).with(name: "Andy")

you could just have:

allow(UserCreator).to receive(:invoke)

post :create, name: "Andy"

expect(UserCreator).to have_received(:invoke).with(name: "Andy")

(Geoff Harcourt) #6

After listening to Avdi Grim talk about this issue/question/problem, I have been trying to make my service classes talk about actions more than roles. So I would probably call it UserCreation rather than UserCreator.

I’ve settled on #perform for generic actions (because it’s then easy to turn the class into a Sidekiq job later) or the verb form of the action, so it’s straightforward to understand: user_creator.create makes sense when you read it later, having forgotten how the class works.


(Ralph Wintle) #7

I was just on the RubyRogues forum and came across pretty much the same thread. It sounds like @andyw8 your approach is taking on the NounAgent.verb convention which Josh Susser mentioned (thread here if you have access: http://parley.rubyrogues.com/t/naming-service-objects-in-rails/521/25).

His example:

user = UserBuilder.build(params[:user])

user = UserRegistrationManager.register(params[:user])
UserRegistrationManager.withdraw(user)

I like that but I’m not overly keen on echoing the class name either so I’ll probably go with run out of personal preference.

I’m thinking I may also ditch the /services directory and try be more accurate with placing my objects int heir appropriate directories i.e. /commands