There seems to be a bit of a blur between the responsibilities of Services and Repositories.
Some questions might be:
- Where do I validate? (Service, Repository or even Model)
- Where do I create an instance of my model to save? (Repo, Controller, Service)
- Where do I put my secondary affects like emailing a user on signup? (Controller, Service, Event Handler?)
- Where do I put x that does y?
I think its a good idea to take a step back and have a look at what each is trying to accomplish.
Whats a service? A service is an object that does something for you.
E.g. An AccountService might signup a user.
Whats a repository? Its a datastore (often persistent). It takes some data and puts it somewhere safe until you want it later.
Ok thats all well and great but I can make a wonderful object that can do all those things and all the other domain logic I need! It creates my user, validates it, saves it to the database and emails the user when I'm done and its still clean and manageable.
Fantastic! simplicity is always best.
But what about later when your class has now grown and you need to add more responsibilities to that class. Now I want my class to tweet me when someone has signed up. The first step will be to move out your database store into a separate class. This way your logic is free from the constraints of how to I persist this wonderful object I've created.
It has three other major advantages as well.
- Now when I test I don't have to wait for my slow database connection. I just mock the repository
- My logic is free from my persistent store. I can switch out to a file store if I want.
- Allows me to follow a hexagonal architecture. By only coding to a repository interface it keeps all external entities outside of my domain logic
Ok so in short this new abstraction I've pulled out of my logic class has one job. Put my data somewhere else until I need it.
Oh dear. I'm explaining two types of classes and I've already used one up and I've still got creating my model, validation, emailing and my new tweeting functionality and I've only got one more abstraction to explain. Thats alright because a service is so generic it does everything. The key is a service should only do 1 main thing.
If I want to sign a user up I'll have a service that signs users up. Lets call it AccountCreator (we don't need to add the suffix 'Service' because what value does that add to the class). But isn't that class still doing everything except the nitty gritty data persistence?. Yes and no. Just bear with me and I'll explain.
AccountCreator will:
- create an instance of User from the the input form data
- Sanitise the input
- Validate the input
- Save if valid
- send an email
- send a tweet
However the AccountCreator won't actually do any of that. It'll just tell other services to do it. I'll go through the list in a bit more detail.
- Create a User instance with a UserFactory
- Tell a UserInputSanitiser to sanitise my input
- Ask a UserAccountValidator if the sanitised data is valid
- Give my User to the UserRepository to store it for later
- Tell my EventDispatcher to let any objects listening that I've created a new user
- The EmailListener hears that a new User has been created and tells the mailer to send an email for it.
- The TweetListener hears that a new User has been created and tells the tweeter to send a tweet.
So really what we have in the end are two different types of abstractions
Internal Services(all my domain logic)
and
Gateways to External Services (The objects that tell something external what to do. Eg. Repositories, mailers and our tweeter).
I hope this has opened up your mind a little about why we create so many of these "excessive" classes.