The Service Layer
Learning Objectives
- Understand the benefits of maintaining separation of concerns in a program
- Appreciate common architecture patterns seen in Spring applications
- Understand what a “Bean” is in context of a Spring application
- Be able to use a Bean within a Spring application
- Understand what the
@Service
annotation is used to signify in a Spring application - Understand what the
@Autowired
annotation is used to signify in a Spring application - Be able to make use of @Service and @Autowired annotations
Introduction
We can finally have some fun with our game! It's not perfect - it won't tell us when the game is won and the mystery word is always going to be "hello" - but we can at least make a guess and get some feedback on it. Most importantly we have built it in a format which will enable us to interact with it in a number of different ways in the future. We could play using a client like Postman, build a web app for it or even use this API as a microservice for another Spring app.
The problem is that we haven't built our game well. We're violating at least two of the SOLID principles (single responsibility and open-closed) but at the moment there isn't a clear solution to the problem. Careful consideration of the application's architecture will help.
Why Separate Our Code?
In previous projects the need for the Single Responsibility Principle has been clear. Indeed it's still obvious in some parts of our current project: a Game
POJO shouldn't have anything to do with a Reply
, so each needs its own class. The distinctions start to break down when we move away from POJOs though. Where exactly do we draw the line with what a controller should do? How much should the models be taking care of?
The Open-Closed Principle should act as a guide for us as well. Recall that a class should be open for extension but closed for modification. Currently our GameController
is failing this test as any new functionality will require significant changes to the codebase. Imagine a scenario where we wanted to play a different game, maybe a number guessing game instead of a word game. The controller would still need to be able to process the request, but at the moment it can only work with our word game.
Adding new rules will therefore require heavy edits. The handleGuess
method is already long and would need another if
-statement to determine which rule set it should follow. Our controller already breaks single responsibility, making one of its methods break the principle as well won't help the situation.
Things will get even worse when we start keeping track of previous game results. Not only will we be adding more responsibilities to the controller, each method will get longer and more convoluted depending on what we're storing and how we store it. We will quickly hit a tipping point where not only do we have to contend with a bloated, brittle controller, we won't even know where to start looking when something breaks. Wouldn't it be great if instead of that mess we could abstract some of the code away into a different class, leaving our controller to call a method when it needed to do something?
@RestController
public class MyController{
@PostMapping(value="/route")
public ResponseEntity createResource(Payload payload){
someMethod(payload)
return new ResponseEntity()
}
}
This is much easier said than done, though. Without an understanding of how to abstract the method and where to abstract it to all we are doing is moving the problem somewhere else.
An Example of Spring Boot Architecture
We have already seen many examples of the request-response cycle, where a client sends a request to a server to process before receiving a response. This separation is often necessary since our application's users are unlikely to be in the same place as the server but even when everything is running on the same system it has benefits. The client only has to make a request and deal with the response, it doesn't need to worry about how one is turned into the other. Likewise the server doesn't care about what's going to happen with the response, it just needs to package it up and send it off.
We may not formalise it in the same way as request-response, but it's entirely possible to take a similar approach internally in a Spring application. By separating our code into "layers" we can determine which classes should be responsible for which actions according to the layer the sit in. Our controllers will only need to send information to a service and deal with whatever is returned, they don't need to worry about the logic. The service classes don't care what happens with the information they send back, they just process an input and send off the results.
There is no single "best" way to structure a Spring application, but as always some patterns will be better than others in certain situations. The structure we will use for our game is laid out at a high level in the diagram below:
Within the server there are three distinct layers:
The Controller Layer
Our controllers sit at the top level of the server and act as gatekeepers, dictating how the clients (in whatever form) can interact with the API. They define the routes which are available to the client and the methods each route will accept, along with any required parameters. They also create the response objects and set their status codes.
When a request is received the controller should not be "hands-on" in the processing of it. Instead it should deserialise whatever payload is sent in the request's body and send it onward to a different part of the application for it to deal with, typically by passing it as an argument to a method. The return value from that method can be sent to the client as the body of a response, or incorporated into a payload which is sent.
Controllers can also be used to determine what the status of a response should be. For example, if a request is received searching for a specific resource the controller would not do the search itself but would have access to the result. If a resource is found the controller may return the value with a 200
status, or if nothing is retrieved it could return an error and 404
. If we are using a templating engine such as Thymeleaf the controller will also be responsible for requesting the view to be sent to the client.
The Service Layer
Typically when a controller passes off a payload to another class it will send it to a service class. These are responsible for the "heavy lifting" of an application and are where most of the business logic will be defined. That logic could be a couple of lines long, it could be a long, complex process. It could even involve acting as a client and making requests to other APIs or microservices.
In our application the service layer will have a couple of jobs. For now its main purpose will be to handle the game logic, moving it away from the controller. This will be the first step towards making our controller capable of playing any game, although that will likely still require a refactoring of how we handle guesses and format replies. It doesn't entirely solve our single responsibility problem though as there is a lot to do in order to play a game.
At the moment all we are doing is creating and updating a Game
object but as our application grows we will also need to access player details and keep track of which words have been used in games. Those will each require their own service to maintain single responsibility. It's beyond the scope of this week, but we could hypothetically have a player log in before playing a game, or compete against another player. Any supporting logic would be defined in a class sitting in the service layer.
The other job our service layer will have is to form a link between the controller and any data storage a client needs to access. Regardless of the type of data storage, directly accessing it is the responsibility of another layer of our application.
The Data Access Layer
The Data Access layer is responsible for exactly what it sounds like: managing the storage and retrieval of data needed by our application. By keeping this independent from the controller and the service layer we enable our application to work with data stored in many different ways. Ultimately our service layer won't care how player records are stored, it will rely on a class in the data access layer to expose methods enabling it to access them.
As we build out our application we will see examples of two ways in whcih we can store data: maintaining collections of the relevant objects internally and connecting to an external database.
The Database
The database in the diagram above is still part of the server but isn't part of the Spring application. We can use both relational and non-relational databases, provided Spring Boot has an appropriate driver for our chosen language. There are a number of ways in which Spring can interact with a database, ranging from entirely auto-generated queries to executing user-defined queries when particular methods are called. Depending on the configuration we may even be able to swap one database for another, update the driver and our app will continue working as if nothing happened.
The Models
Sitting next to our three layers in the diagram are the models. As we have already seen they are the classes which define the data moving around our application and as such are needed at every step of the process. We have already seen how we can define DTOs to facilitate communication between client and server and this structure can be used for communication between the layers of the server as well. In a future lesson we will see how to annotate models in such a way that we can automatically generate a database schema for the data access layer to use.
Creating Beans
Before we can start splitting our application up we need to establish how the different layers will be able to communicate with each other. When we move the logic from GameController
to GameService
the logical next step is to instantiate a GameService
object somewhere in the controller to enable access to the methods. When we do this, though, we start to push against another SOLID principle - this time it's dependency inversion.
If a controller is responsible for instantiating a required service then the two classes have become tightly coupled. In general we try to avoid this, ensuring that the existance of one object doesn't depend on another. Spring gives us a way to keep our classes loosely coupled by handling the instantiation of all the needed objects elsewhere. Using a process known as dependency injection we can then use those objects in the appropriate places in our application.
This has been happening already, we just haven't realised we're doing it. If we stop and think, though, there's a big question that pops into our heads: how have we been calling the methods in GameController
when we haven't created one with new GameController()
anywhere in our app?
The object is created and managed for us by Spring, making it an example of a bean. Beans are used in many types of Java application, not just Spring, and have similar requirements to POJOs. In addition to those requirements they must implement the Serializable
interface, which is taken care of for us when working with Spring beans. Not everything becomes a bean when working with Spring, though - we need to use annotations to identify them.
Spring includes the @Bean
annotation which identifies a class as one which can be instantiated as a Spring bean. Annotations are like regular classes in that they can be extended, and Spring leverages this to give more specificity to some of the annotations we can use. When we created our controller we annotated it using @RestController
which is a sub-class of @Bean
but adds some controller-specific logic. There are variations (such as @Controller
) which are implemented in slightly different ways.
When our application starts, every class with an annotation which extends @Bean
is instantiated by Spring and ready to be injected wherever it is required. In the case of our controllers we don't need to explicitly handle the injection, which is why we can immediately access our routes` but this generally won't be the case. We will need to not only identify the beans, but also explicitly state where they should be injected.
Using a @Service
Bean
Now we know how to manage the objects we will be creating we can start to dismantle our monster controller. The first step is to create a services
package to hold the files, followed by a GameService
class.
// services/GameService.java
@Service
public class GameService {
}
This time we use the @Service
annotation for the class declaration which also extends @Bean
. Our logic will be moving from GameController
across to GameService
which means the properties it depends on need to move too. We will move game
, currentWord
and guessedLetters
into our new service class and set up a constructor along with getters and setters.
// services/GameService.java
@Service
public class GameService {
// copy/paste properties from `GameController`
private Game game;
private String currentWord;
private List<String> guessedLetters;
public GameService() {
}
// getters & setters
}
// services/GameService.java
@Service
public class GameService {
// ...
public Reply startNewGame(){
this.game = new Game("hello");
this.currentWord = "*****";
this.guessedLetters = new ArrayList<>();
return new Reply(
this.currentWord,
"Started new game",
false
);
}
}
We also need to transfer the method to check a guess.
// services/GameService.java
@Service
public class GameService {
// ...
public Reply processGuess(Guess guess){
// create new Reply object
Reply reply;
// Check if game has started
if (game == null) {
reply = new Reply(
this.currentWord,
String.format("Game has not been started"),
false
);
return reply;
}
// check if letter has already been guessed
if (this.guessedLetters.contains(guess.getLetter())) {
reply = new Reply(
this.currentWord,
String.format("Already guessed %s", guess.getLetter()),
false);
return reply;
}
// add letter to previous guesses
this.guessedLetters.add(guess.getLetter());
// check for incorrect guess
if (!game.getWord().contains(guess.getLetter())) {
reply = new Reply(
this.currentWord,
String.format("%s is not in the word", guess.getLetter()),
false);
return reply;
}
// process correct guess
String runningResult = game.getWord();
for (Character letter : game.getWord().toCharArray()) {
if (!this.guessedLetters.contains(letter.toString())) {
runningResult = runningResult.replace(letter, '*');
}
}
this.currentWord = runningResult;
reply = new Reply(
this.currentWord,
String.format("%s is in the word", guess.getLetter()),
true);
// return result
return reply;
}
// ...
}
Now instead of defining it itself our controller can call upon the methods defind in GameService
. The methods to start a game and process a guess are much shorter:
// controllers/GameController.java
@PostMapping
public ResponseEntity<Reply> newGame(){
Reply reply = gameService.startNewGame();
return new ResponseEntity<>(reply, HttpStatus.CREATED);
}
@PatchMapping
public ResponseEntity<Reply> handleGuess(@RequestBody Guess guess) {
Reply reply = gameService.processGuess(guess);
return new ResponseEntity<>(reply, HttpStatus.OK);
}
Our two GET
requests can also use methods from the service to get what they need.
// controllers/GameController.java
@GetMapping
public ResponseEntity<Reply> getGameStatus(){
Reply reply;
// check if game has started
if (gameService.getGame() == null) {
reply = new Reply(
gameService.getCurrentWord(), // MODIFIED
"Game has not been started",
false
);
} else {
reply = new Reply(
gameService.getCurrentWord(), // MODIFIED
"Game in progress.",
false
);
}
return new ResponseEntity<>(reply, HttpStatus.OK);
}
We now have a problem though. If we start our application we can still make requests to the same routes but we get 500
errors every time. Something has broken in our server.
The problem lies with our beans. Although @Service
and @RestController
both extend @Bean
they are managed by Spring in different ways. Although Spring still instantiates a @Service
it doesn't handle the injection in the same way s it does for a @RestController
, which means that although our GameService
has been instantiated we can't access it from anywhere. We need to explicitly inject it into GameController
which can be done using another annotation.
// controllers/GameController.java
@RestController
public class GameController{
@Autowired
GameService gameService
// ...
}
The @Autowired
annotation indicates that a bean of the given type should be injected into this class. With this in place we can now run our application as before with no noticeable difference to the client, but our architecture is much cleaner. Any time we want to inject a bean into one of our classes we will use @Autowired
to do so.
Next Steps
We now have a much clearer separation of responsibilities between the layers of our application.
- The controller is only responsible for receiving requests and sending responses. The only logic affects the structure of a response, with the determining factor being a result of something happening in the service layer.
- The service is handling the more complex logic, such as for checking guesses. Any information received in a request is passed to the service by the controller. The service sends an object back to the controller detailling the outcome.
- Both use the models to create POJOs representing the data moving through the application.
We're still breaking the Single-Responsibility Principle though, since the service layer is storing data as well as handling the logic. It's time to add another layer to the architecture and separate the responsibilities even further.