The goal of these exercises is to practise these concepts:
For the exercises below, we've provided the starter project above.
The goal of this exercise is to decouple tightly-coupled code by applying the following software design principles and patterns:
For this exercise, we've provided starter code in the software-design directory. It contains a small program that plays a simulated game between two players rolling a dice.
We won't be changing the functionality of the application at all, but refactoring it to be loosely coupled.
In your terminal, ensure you are in the root project directory, then run the following command to execute the application:
./mvnw --projects software-design -q clean compile exec:javaYou should see output similar to this:
Game started. Target score: 30
Player 1 rolled a 4
Player 2 rolled a 5
Player 1 rolled a 4
Player 2 rolled a 5
Player 1 rolled a 4
Player 2 rolled a 6
Player 1 rolled a 5
Player 2 rolled a 1
Player 1 rolled a 6
Player 2 rolled a 3
Player 1 rolled a 4
Player 2 rolled a 2
Player 1 rolled a 4
Player 2 rolled a 4
Player 1 wins!Open the software-design/src/main/java/com/cbfacademy/ directory.
The DiceGame class calls dicePlayer.roll() in order to complete the play() method. DiceGame can't function without a DicePlayer instance, so we say that DiceGame is dependent on DicePlayer or that DicePlayer is a dependency of DiceGame.
The first step towards decoupling our code is to invert the control flow by using the Factory pattern to implement IoC.
- Examine the
PlayerFactoryandGameFactoryclasses. - Replace the
new DicePlayer()statements inDiceGamewithPlayerFactory.create(). - Replace the
new DiceGame()statement inAppwithGameFactory.create(). - Run the application again to confirm you get the same output as before.
- Commit your changes.
This delegated responsibility to the factory allows us to decouple the DiceGame class from the DicePlayer class.
The Dependency Inversion Principle states that:
- High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Currently, our DiceGame class (high-level module) depends on DicePlayer (low-level module). This is a violation of the Dependency Inversion Principle, so we must replace this concrete dependency with an abstraction (interface or abstract class).
- Examine the
GameandPlayerinterfaces. - Modify the
DiceGameclass to implement theGameinterface and theDicePlayerclass to implement thePlayerinterface. - Modify the
GameFactoryandPlayerFactoryclasses to return instances of theGameandPlayerinterfaces rather than the concrete classes. - Modify the
gamemember inAppto be of typeGamerather thanDiceGame. - Modify the
player1andplayer2members inDiceGameto be of typePlayerrather thanDicePlayer. - Run the application again to confirm you get the same output as before.
- Commit your changes.
We have now implemented DIP, where a high-level module (DiceGame) and low-level module (DicePlayer) are both dependent on an abstraction (Player). Also, the abstraction (Player) doesn't depend on details (DicePlayer), but the details depend on an abstraction.
We have now inverted control and introduced abstraction, but our classes are still tightly coupled to the factory classes. Let's resolve this by instead injecting dependencies into the constructor of the DiceGame class.
- Modify the
DiceGameconstructor to accept twoPlayerparameters. - Modify the
GameFactory.create()method to accept twoPlayerparameters and inject them into theDiceGameconstructor. - Modify the
mainmethod inAppto create twoPlayerinstances (usingPlayerFactory) and pass them to theGameFactory.create()method. - Run the application again to confirm you get the same output as before.
- Commit your changes.
By injecting the Player instances into the DiceGame constructor, we have now successfully decoupled DiceGame from DicePlayer.
While we've now decoupled our code, we still have to create instances of our interfaces using multiple factory classes. In a real-world application with numerous interfaces defined, this can quickly become a maintenance nightmare. To address this, we can use a IoC Container to manage our dependencies.
- Examine the
SimpleContainerclass. It may contain code that looks unfamiliar, but focus on the comments describing the behaviour of theregisterandcreatemethods. - Add the following method to the
Appclass:
private static SimpleContainer initialiseContainer() {
SimpleContainer container = new SimpleContainer();
// Register mappings for any required interfaces with their concrete implementations
return container;
}- Modify the
initialiseContainermethod to register mappings for theGameandPlayerinterfaces with their concrete implementations in the container, e.g.container.register(Game.class, DiceGame.class) - Add a call to
initialiseContainerin themainmethod ofApp, before any factory method calls. - Replace the call to
GameFactory.create()withcontainer.get(Game.class) - Remove the calls to
PlayerFactory.create() - Run the application again to confirm you get the same output as before.
- Commit your changes.
By using a container, we're able to simplify our code and eliminate the need for multiple factory classes. This makes our code more modular, maintainable and easier to understand.
The goal of these exercises is to build a RESTful API service that allows users to manage IOUs (I Owe You agreements) using Spring Boot.
- Login to MySQL:
mysql -u root -pπ‘ Note: If your root user doesn't have a password set, omit the
-pflag.
- Create a new database:
CREATE DATABASE IF NOT EXISTS springbootexercise;
exit;- Open this pre-configured Initializr project. Review the configured settings, but do not make any changes. Click "Generate" to download a zipped project
- Extract the downloaded zip file
- Ensure that you are in the root project directory in the terminal, then copy the contents of the extracted directory to your
rest-apisubdirectory. IMPORTANT: Do NOT copy the extracted files using Finder as not all extracted files may be correctly moved. Use the appropriate command to copy the files:- macOS (zsh):
cp -r [extracted directory]/* [extracted directory]/.[^.]* rest-api/, e.g.cp -r ~/Downloads/springbootexercise/* ~/Downloads/springbootexercise/.[^.]* rest-api/ - macOS (bash):
cp -R [extracted directory]/. rest-api/, e.g.cp -R ~/Downloads/springbootexercise/. rest-api/ - Ubuntu/Windows (Git Bash):
cp -r [extracted directory]/* [extracted directory]/.* ., e.g.cp -r ~/Downloads/springbootexercise/* ~/Downloads/springbootexercise/.* .
- macOS (zsh):
- Delete the extracted directory and the downloaded zip file
- Open your repository in VS Code
- Add the following values to
rest-api/src/main/resources/application.properties:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=true
spring.config.import=optional:./local.properties- In order to prevent sensitive values from being committed to version control, add a new entry to the .gitignore file:
local.properties
- Create a new file at
rest-api/src/main/resources/local.propertiesand paste in the following:
spring.datasource.url=jdbc:mysql://localhost:3306/springbootexercise
# Replace "root" with your database user, if applicable
spring.datasource.username=root
# Specify your database user's password, if applicable. If your database user doesn't have a password set, delete the line below
spring.datasource.password=YOUR_MYSQL_PASSWORD- Replace the username and password values with your MySQL credentials. IMPORTANT: Ensure there are no spaces before or after the password.
To start the API, run the following command from the root project directory:
./mvnw --projects rest-api spring-boot:runIf successful, you should see output that ends similarly to the following:
2024-04-12T11:49:59.055-04:00 INFO 39975 --- [Spring Boot Exercise] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2024-04-12T11:49:59.059-04:00 INFO 39975 --- [Spring Boot Exercise] [ main] c.c.r.SpringBootExerciseApplication : Started SpringBootExerciseApplication in 1.493 seconds (process running for 1.638)
IMPORTANT: If everything is working correctly, the output will appear "stuck" and the terminal won't return until you stop the application, which should now be running at http://localhost:8080/api/ious.
Stop the application by pressing Ctrl + C
- Create an ious package in the main springbootexercise package
- Create an
IOUentity class that maps to the "ious" table and has the following fields:UUID idString borrowerString lenderBigDecimal amountInstant dateTime
- Ensure the
idfield is set as the primary key and values are generated using the appropriate strategy for aUUIDfield - Define a constructor that accepts the following parameters:
IOU(String borrower, String lender, BigDecimal amount, Instant createdAt) - Define a default (parameterless) constructor that calls the parameterised constructor internally. Consider what appropriate default values should be passed to the parameters
- Create getter and setter methods for each field, except
id, which should only have a getter - Create an
IOURepositoryinterface that extendsListCrudRepository<IOU, UUID> - If it's not already running, start your API with
./mvnw --projects rest-api clean spring-boot:run. Check the output and confirm there are no errors - Check your database contains an "ious" table with the correct columns and data types
- Commit your changes
- Create an IOUService class that accepts an IOURepository as a dependency and implements the following methods:
List<IOU> getAllIOUs()IOU getIOU(UUID id) throws NoSuchElementExceptionIOU createIOU(IOU iou) throws IllegalArgumentException, OptimisticLockingFailureExceptionIOU updateIOU(UUID id, IOU updatedIOU) throws NoSuchElementExceptionvoid deleteIOU(UUID id)
- Create an
IOUControllerclass that implements the endpoints below. Ensure your service class is injected as a dependency and apply the appropriate annotations - Start your API and confirm there are no errors
- Commit your changes
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/ious | Retrieve a list of (optionally filtered) IOUs |
| GET | /api/ious/{id} | Retrieve a specific IOU by its ID |
| POST | /api/ious | Create a new IOU |
| PUT | /api/ious/{id} | Update an existing IOU by ID |
| DELETE | /api/ious/{id} | Delete an IOU by ID |
You can now test your endpoints using Postman or your preferred REST client at http://localhost:8080/api/ious
The JSON representation of an IOU that you'll get in responses or provide in the request body for POST and PUT requests will resemble the following:
{
"id": "d1415cfc-dbd9-4474-94fc-52e194e384fa",
"borrower": "John Doe",
"lender": "Alice Smith",
"amount": 100.0,
"dateTime": "2023-11-02T14:30:00Z"
}π‘ Note: Remember that the
idproperty may not be needed for all request types.
- Create an
iouspackage inside therest-api/src/test/java/com/cbfacademy/springbootexercisepackage - Download the test suite and copy to the test
iouspackage asIOUControllerTest.java - Configure H2 database for testing: Add the H2 database dependency to your
rest-api/pom.xmlfile. Insert this dependency in the<dependencies>section:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>- Create test configuration: Create a new directory
rest-api/src/test/resourcesand add anapplication.propertiesfile with the following content:
# Test configuration using H2 in-memory database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# H2 Console for debugging (optional)
spring.h2.console.enabled=true
# JPA/Hibernate configuration for H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true- Run the tests with
./mvnw --projects rest-api clean test - Examine the results. Do any tests fail? If so, what reasons are given? Modify your code so all tests pass
- Commit your changes
π‘ Note: The H2 configuration ensures tests use an in-memory database for speed and isolation, while your production application continues to use MySQL. The
create-dropsetting means the database schema is recreated for each test run, ensuring clean test conditions.
- Create a new API endpoint to return IOUs for a specific borrower:
- Create a method in your repository interface called
findByBorrowerthat accepts a stringborrowerparameter. - Create a method in your service class called
getIOUsByBorrower. - Extend the
getIOUSmethod of your controller to accept an optional query string parameter, e.g.:getIOUs(@RequestParam(required = false) String borrower) - Check the value of the
borrowerparameter to determine whether to call the existing service method or the new, filtered, one
- Create a method in your repository interface called
- Test the modified endpoint
- Commit your changes
Modify the /api/ious endpoint to filter IOUs for a specific lender, defined as an optional query string parameter.
- Create a new API endpoint to return IOUs with above-average value:
- Create a method in your repository interface called
findHighValueIOUs. - Define a native
@Queryannotation that will return all IOUs with an above average value. Hint: create a subquery using theAVGfunction - Create a method in your service class called
getHighValueIOUs. - Create a
getHighValueIOUSmethod in your controller, mapped to the/highpath.
- Create a method in your repository interface called
- Test the new endpoint
- Commit your changes
- Create a new endpoint at
/lowto return IOUs that are below or equal to the average value. Implement the repository method using JPQL instead of SQL - Commit your changes
- πΈ Commit frequently and use meaningful commit messages. A granular, well-labelled history becomes an increasingly valuable asset over time.
- π΅ Use feature branches. Build the habit of isolating your changes for specific tasks and merging them into your default branch when complete.
- π¦ Use consistent naming conventions. Choose easily understandable names and naming patterns for your classes, functions and variables.
- π Keep your code tidy. Using the built-in formatting of VS Code or other IDEs makes your code easier to read and mistakes easier to spot.
- π Read the docs. Whether via Intellisense in your IDE, or browsing online documentation, build a clear understanding of the libraries your code leverages.
- π Don't wait until the last minute. Plan your work early and make the most of the time available to complete the assessment and avoid pre-deadline palpitations.
- π Ask. π For. π Help! π Your mentors, instructors and assistants are literally here to support you, so make use of them - don't sit and struggle in silence.
Best of luck! Remember, it's not just about the destination; it's the journey. Happy coding! π