Many developers are accustomed to the design pattern in which we break our application code into the models/entities layer, the repository layer, the service layer, and the controllers.
The reason for the entities layer is pretty straightforward: you must declare the fields(and their types) of entities in your application. In some applications, you can also describe the database schema in your entity class. You place your queries in the repository layer and use them to retrieve data, while your controllers contain handlers for each route and HTTP method. But what about Services?
I occasionally have service classes that appear to have no use other than to satisfy the need to have a service layer before my repositories.
I recently discovered a great reason to have service layer classes. A project I was working on had no clearly defined service layer. I needed to write code to pull data from different repos, process it and return the result. For the sake of this article, let's assume that I first wrote this code in the controller class.
const repo1 = new Repo1()
const repo2 = new Repo2()
@rest('/demo')
class DemoController {
@get('/result')
getResult(req) {
// handler for GET /demo/result
const stuff = repo1.findById(req.body.id)
const moreStuff = repo2.findAllCreatedBefore(req.body.time)
// do some processing here
return { result }
}
}
Then I realized I needed it somewhere else, so I transferred it to a different file and wrapped the code in a function.
// process-data file
const repo1 = new Repo1()
const repo2 = new Repo2()
export function processData(id, time) {
const stuff = repo1.findById(id)
const moreStuff = repo2.findAllCreatedBefore(time)
// do some processing here
return result;
}
Now that I can use it everywhere, isn't my job done? No, not exactly. There is only one problem: How will I test this function?
The function has dependencies (the repositories) that are not part of its parameters which will make testing difficult. This function needs to use a set of mocked dependencies in my test cases. How do I go about doing this? Wouldn't a class be appropriate here?
// process-data file
export class DataProcessor {
private repo1
private repo2
constructor(repo1, repo2) {
this.repo1 = repo1
this.repo2 = repo2
}
processDataByIdCreatedBeforeTime(id, time) {
const stuff = this.repo1.findById(id)
const moreStuff = this.repo2.findAllCreatedBefore(time)
// do some processing here
return result;
}
}
export const processor = new DataProcessor(new Repo1(), new Repo2())
// test DataProcessor
it('should process the data correctly', () => {
const processor = new DataProcessor(mock1, mock2)
// ...some testing code
const result = processor.processDataByIdCreatedBeforeTime(id, time)
assert.equal(result, expectedResult)
})
In the example above, doesn't the DataProcessor class look like a Service layer class? The need to test the data processing logic eventually led to the creation of the service layer class. So, what should we take away?
- You need the Service layer separate from your repositories and controllers so you can unit-test your business logic code
- You also need the Service layer so that your business logic code can be reused in other parts of your application
- Consider using Test Driven Development?
This is my opinion. I suspect that there may be contrary ideas or I may have missed something, in that case, feel free to share in the comments.
Thank you for reading.
Top comments (2)
Wonderful one Novo
And I never thought of it from this angle. Really nice one.