What is good service object?
- Single responsibility
- Checks it’s input
- Command/query separation
- Same level of abstraction
Example of good service object
Let’s start by giving a simple example:
As we see above,
UserCreator is single responsible for creating user. No email
sending or whatever. If you need, for example, to send welcome email it is
probably should be done from another service, say
The smaller construction blocks in your program the less pain it would
be to change them.
Checks it’s input
It’s simple, don’t check email address for being nil outside of the service. This will spread such checks all across the codebase. Object constructor is the one who’s responsible for setting object invariant - state in which object can perform without braking. If your service needs email address to work correctly and it is not given - brake early, throw exception from constructor.
Usually objects have two types of methods: commands and queries. Commands change something and return nothing, queries in opposite return current state without changing anything. This separation brings clarity in how and when you use particular method, so you don’t endup in situation when asking object for it’s state also changes it or sends email.
Same level of abstraction
Term abstraction is about hiding details. In
#create_user we hide checking user existence and saving user details inside
#save_user methods. We’d brake this rule if some details are still left inside
In other words you should be able to inspect input parameters, which usually got supplied through constructor, hense you should have attribute readers for every input parameter. This especially handy upon debugging.
According to Sandi Metz, you should test object query methods by asserting returned result, command methods - by asserting direct public side effect and outgoing command methods, by expecting to send them. Usually, I would test such service as so:
Trying to visualize service object I came out with this picture: