SOLID Principles in Spring Boot Part II
This article will take you through the remaining SOLID principles as we build out a light-weight, cryptocurrency digit wallet API in Spring Boot. By the end of this assignment you will have an understanding of the Open-closed Principle (OCP), the Dependency Inversion Principle (DIP), and the Interface Segregation Principle (ISP).
SOLID Principles
To review, SOLID Principles are a set of software design principles in object-oriented programming that are used to make software more maintainable, extensible, and flexible.
The theory of SOLID principles was introduced by Robert C. Martin in his paper Design Principles and Design Patterns and consists of 5 core principles:
Single-responsibility principle: A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.
Open-closed principle: A module should be open for extension but closed for modification.
Liskov Substitution principle: Instances of a class should be replaceable by instances of a derived class without altering correctness of the program.
Interface Segregation principle: Many client-specific interfaces are better than one general-purpose interface.
Dependency Inversion principle: Depend upon abstractions. Do not depend upon concretions.
In this assignment we will deal with the Open-closed principle, Dependency Inversion principle, and the Interface Segregation principle.
Before we start, make sure that you’ve completed the setup and Part 1 prior to starting Part 2.
Starter Code
After completing Part 1 of the assignment, your project should have the following classes and directory structure below 👇:
.../restservice/
api/
DigitalWalletController.java
coins/
Bitcoin.java
BitcoinRobust.java
wallet/
DigitalWallet.java
Assignment
This assignment will focus on decoupling the architecture we implemented in Part 1 to support future requirements of the DigitalWallet.
Open-closed Principle
The first part of the assignment will deal with the Open-closed principle.
Robert C. Martin, one of the father’s of Agile software development described that the Open-closed principle was the most important of the five SOLID principles. This principle is based off the idea that:
A module should be open for extension but closed for modification.
In essence, you can use abstract classes or interfaces to allow for different implementations without changing the code that uses them. Since interfaces are built into Java to handle Multi-Inheritance, they lend themselves well for upholding the Open-closed principle. For our use-case, interfaces will be closed for modification and we will use new classes to extend the functionality of our system.
To demonstrate this principle, we will revisit the DigitalWallet class and reimagine how we could make it more flexible and loosely coupled to handle future use cases.
The three methods implemented in the DigitalWallet class: processTransaction, zero, and accountBalance are all configured to handle Bitcoin. If we wanted to add other types of coins to our wallet, we’d need to refactor our DigitalWallet class so let’s do that now.
Instructions 🎒
-
Create an interface called
ICointhat speficies the 3 API methods in theDigitWalletclass:processTransactionzeroaccountBalance
-
Implement the method on the
Bitcoinclass (Note: you only have to implement the interface on the baseBitcoinclass sinceBitcoinRobustis a child class) -
Change the return type of the three API methods in the
DigitalWalletclass:processTransaction,zero, andaccountBalance, to the new interfaceICoin. -
Change the return type of the endpoints in
DigitalWalletControllertoICoin.
After this refactor, test all of the API endpoints and ensure that the output is still the same.
At this point, the DigitalWallet class is almost heeding to the Open-closed principle but there is one more consideration we must take into account to fully decouple this class from implementation details and ensure that the class is open for extension yet closed for modification.
Dependency Inversion principle
The Dependency Inversion principle specifies that modules should:
Depend upon abstractions. Do not depend upon concretions.
In the DigitalWallet class, although we created and implemented the ICoin interface—the DigitalWallet class is still relying on the Bitcoin singleton in all three of the API methods. This is an example of relying on a concretion instead of an abstraction and is also preventing the class from complying with the Open-closed principle since you would need to modify the DigitalWallet class if you wanted to add another coin to the wallet like Ethereum 😉.
Instructions 🎒
- Remove the references of the
Bitcoinsingleton from theDigitalWalletclass and refactor the three API methods to have a parameterICoin cointhat is used to call the correspondingICoinmethod. - In
DigitalWalletController, pass theBitcoinsingleton into theDigitalWalletmethods for each endpoint.
After this refactor, the DigitalWallet class is dependent completely on abstraction now that we’ve inverted the dependency on the Bitcoin singleton to the client that invokes the DigitalWallet methods, making this class loosely coupled and flexible.
We’re now adhering to the Open-closed principle as well as the Dependency Inversion principle.
Ethereum
Ethereum is a global, open-source platform for decentralized applications. It runs off a cryprocurrency called Ether (ETH) which is used to power thousands of Distributed applications (Dapps).
Now that we’ve extended our DigitalWallet class, let’s go on and add another coin to our wallet.
Instructions 🎒
-
Create a class called
Ethereumthat implements theICoininterface.-
Make the
Ethereumclass a singleton since this will only be used to manage your Ethereum. -
Add two static constants to the
Ethereumclass:NAMEWHITE_PAPER
-
Add one non-static attribute to the
Ethereumclass:ether
-
Create two static attributes constants named:
ETHER_USD, which will hold the price of Ethereum in USD (e.gETHEREUM_USD= 524.44)TRANSACTION_FEE_USD, which will hold the price of the transaction fee in USD (e.gTRANSACTION_FEE_USD= 11.66)
-
Create three methods that map to the three endpoints defined in
DigitalWalletController:- Ethereum processTransation( double requestedEther ), This method will now need to deduct a transaction fee off the amount of Bitcoin in the requested transaction. Note: you should also take the transaction fee into account when checking to see whether or not there are sufficient funds for the transaction.
- Ethereum setZero()
- Ethereum accountBalance()
-
Create four methods that will be used to serialize the Java object to a JSON response.
- public String getName()
- public double getEther()
- public String getWhitePaper()
- public double getTransactionFeeUSD()
-
-
Refactor the endpoints in
DigitalWalletControllerto be prefixed with/btc/e.g@GetMapping("/btc/transaction")- You should also rename the underlying methods of these endpoints e.g
wallet(...)➡️btc_balance(...)
- You should also rename the underlying methods of these endpoints e.g
-
Create a reference to the
Ethereumsingleton in theDigitalWalletControllerclass. -
Create three new endpoints/methods in
DigitalWalletControllerall prefixed with/eth/:@GetMapping("/eth/transaction"), public ICoin eth_transaction(..)@GetMapping("/eth/balance"), public ICoin eth_balance(..)@GetMapping("/eth/zero"), public ICoin eth_zero(..)
Note: All of the Ethereum endpoints should pass the Ethereum singleton into the respective DigitalWallet methods.
Working through extending the service by adding a whole different cryptocurrency to our digital wallet shows the benefits of SOLID design.
We were able to extend the servce my implementing an Ethereum class without having to even touch the DigitalWallet class while making
use of 4 out of the 5 SOLID principles.
However, we still have yet to cover the I in SOLID.
Interface Segregation Principle
The Interface Segregation Principle (ISP) is one of the most commonly used SOLID principles. The principle states:
Many client-specific interfaces are better than one general-purpose interface.
Thus far, we have only made use of one interface: ICoin. Placing all of the attributes that a coin must have in order
to function correctly in our Digital Wallet.
Interfaces have many benefits, my favorite being composability which relates directly to ISP. By breaking general-purpose interfaces up into smaller single-purpose interfaces, it gives your service substantially more flexibility since there is no limit to the number of interfaces that a class can implement. It doesn’t make sense to create an interface for every method that you have on a single class, that would become difficult to maintain and largely unnecessary. However, if you have two classes that need to implement similar functionality and they’re not strictly related—this would be an excellent use case to define an interface and describe the functionality between the two classes.
In our use-case, Ethereum is much more than just a cryptocurrency. It’s a full-blown open-source platform that give developers the ability to write distributed applications (Dapps) with blockchain technology. Specifically, Ethereum has this entity called a smart-contract which is code that runs on the Ethereum blockchain.
To demonstrate the ISP, let’s implement Ethereum Smart Contracts in our service.
Instructions 🎒
-
Create a new class called
SmartContract:- Create a non-static attribute called
contract(This will represent a stringified program that will be run on the Ethereum blockchain) - Create a non-default constructor that takes in a string parameter called
contractand set the class attributecontractequal to the parameter. - Create a “getter” for the
contractattribute.
- Create a non-static attribute called
-
On the
ICoininterface add a new method calledaddContract( String contract ) -
In the
Ethereumclass:- Create a new attribute that is a list of
SmartContracts calledsmartContracts - Implement the new
addContract( String contract )method, creating aSmartContractobject, then adding this object to thesmartContractslist. - Create a getter for the
smartContractsattribute:public List<SmartContract> getSmartContracts()
- Create a new attribute that is a list of
-
In the
DigitalWalletclass:- Create a new method
addContract( ICoin coin, String contract )that calls thecoinaddContractmethod.
- Create a new method
-
In the
Bitcoinclass you will need to also implement theaddContract(..)method now that it’s specified on theICoininterface.. even though Bitcoin does not offer support for smart contracts. For this method do not add any implementation, just returnnull. -
In the
DigitalWalletControllerclass, add the endpoint below 👇
@GetMapping("/eth/add_contract")
public ICoin eth_add_contract(
@RequestParam(value = "code", defaultValue = "0" )
String code )
{
try
{
return DigitalWallet
.getInstance()
.addContract( eth, code );
}
catch( Exception e )
{
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, e.toString() );
}
}
This endpoint will map the /eth/add_contract/ endpoint to the addContract method via an HTTP - GET request that has a parameter “code”.
To test this new endpoint and to verify that this new functionality is working as intended type: http://localhost:8080/eth/add_contract?code=%22some%20code%20in%20here%22
This simulates adding a smart contract as a string “some code in here” that will be run as a smart contract on the Ethereum blockchain. If everything was setup correctly, you should see the following response:
{"smartContracts":[{"contract":"\"some code in here\""}],"name":"Ethereum","eth":0.0,"transactionFeeUSD":11.66,"whitePaper":"https://blockchainlab.com/pdf/Ethereum_white_paper-a_next_generation_smart_contract_and_decentralized_application_platform-vitalik-buterin.pdf"}
Note: The order of the fields in the JSON response does not matter.
Circling back to the Interface Segregation Principle, we just ran into one of the primary issues this principle is designed to solve.
Bitcoin doesn’t support smart contracts.
But since we added the addContract method onto the ICoin interface, we had to implement the addContract method on the Bitcoin class.. even though this doesn’t make sense since Bitcoin doesn’t support smart contracts.
To resolve this, let’s create a new interface for handling smart contracts since smart contracts are a hot attribute of other cryptocurrencies outside of Ethereum.
Instructions 🎒
-
Create a new interface called
ISmartContract- Specify a
addContract( String contract )method on the interface
- Specify a
-
Remove the
addContract(..)method from theICoininterface -
Implement the interface on the
Ethereumclass -
Remove the
addContract(..)method from theBitcoinclass sinceaddContract(...)is no longer onICoin -
Modify the return type of
addContractin theDigitalWalletclass to be of typeISmartContract -
Modify the return type of the
/eth/add_contractendpoint inDigitalWalletControllerto be of typeISmartContract
After you refactor this, ensure that the endpoint still behaves correctly and responds with the correct information.
Congrats, you’ve now applied the ISP in practice.
Submission
Submit your assignment on Canvas with your whole directory as a zip file <lastName-section>-spring-pt1.zip e.g Smith-SectionC-spring-pt1.zip