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:java
You 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
PlayerFactory
andGameFactory
classes. - Replace the
new DicePlayer()
statements inDiceGame
withPlayerFactory.create()
. - Replace the
new DiceGame()
statement inApp
withGameFactory.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
Game
andPlayer
interfaces. - Modify the
DiceGame
class to implement theGame
interface and theDicePlayer
class to implement thePlayer
interface. - Modify the
GameFactory
andPlayerFactory
classes to return instances of theGame
andPlayer
interfaces rather than the concrete classes. - Modify the
game
member inApp
to be of typeGame
rather thanDiceGame
. - Modify the
player1
andplayer2
members inDiceGame
to be of typePlayer
rather 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
DiceGame
constructor to accept twoPlayer
parameters. - Modify the
GameFactory.create()
method to accept twoPlayer
parameters and inject them into theDiceGame
constructor. - Modify the
main
method inApp
to create twoPlayer
instances (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
SimpleContainer
class. It may contain code that looks unfamiliar, but focus on the comments describing the behaviour of theregister
andcreate
methods. - Add the following method to the
App
class:
private static SimpleContainer initialiseContainer() {
SimpleContainer container = new SimpleContainer();
// Register mappings for any required interfaces with their concrete implementations
return container;
}
- Modify the
initialiseContainer
method to register mappings for theGame
andPlayer
interfaces with their concrete implementations in the container, e.g.container.register(Game.class, DiceGame.class)
- Add a call to
initialiseContainer
in themain
method 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
-p
flag.
- 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-api
subdirectory. 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.properties
and 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:run
If 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
IOU
entity class that maps to the "ious" table and has the following fields:UUID id
String borrower
String lender
BigDecimal amount
Instant dateTime
- Ensure the
id
field is set as the primary key and values are generated using the appropriate strategy for aUUID
field - 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
IOURepository
interface 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 NoSuchElementException
IOU createIOU(IOU iou) throws IllegalArgumentException, OptimisticLockingFailureException
IOU updateIOU(UUID id, IOU updatedIOU) throws NoSuchElementException
void deleteIOU(UUID id)
- Create an
IOUController
class 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
id
property may not be needed for all request types.
- Create an
ious
package inside therest-api/src/test/java/com/cbfacademy/springbootexercise
package - Download the test suite and copy to the test
ious
package asIOUControllerTest.java
- Configure H2 database for testing: Add the H2 database dependency to your
rest-api/pom.xml
file. 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/resources
and add anapplication.properties
file 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-drop
setting 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
findByBorrower
that accepts a stringborrower
parameter. - Create a method in your service class called
getIOUsByBorrower
. - Extend the
getIOUS
method of your controller to accept an optional query string parameter, e.g.:getIOUs(@RequestParam(required = false) String borrower)
- Check the value of the
borrower
parameter 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
@Query
annotation that will return all IOUs with an above average value. Hint: create a subquery using theAVG
function - Create a method in your service class called
getHighValueIOUs
. - Create a
getHighValueIOUS
method in your controller, mapped to the/high
path.
- Create a method in your repository interface called
- Test the new endpoint
- Commit your changes
- Create a new endpoint at
/low
to 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! π