Scala Tutorial - Learn How To Use Traits For Dependency Injection
Overview
In this tutorial, we will learn how to use traits for dependency injection using the standard Scala library and without having to import any third party tools and libraries.
We will build on what we have learned from the previous tutorials on Learn How To Create And Extend Trait, Learn How to Create Trait With Type Parameters and Learn How To Extend Multiple Traits.
Steps
1. Create a trait which knows how to do create, read, update and delete operations CRUD to a given database
Let's start with defining a trait called DonutDatabase which will provide the method signatures to represent a Data Access Layer.
println("Step 1: Create a trait which knows how to do create, read, update and delete operations CRUD to a given database")
trait DonutDatabase[A] {
def addOrUpdate(donut: A): Long
def query(donut: A): A
def delete(donut: A): Boolean
}
NOTE:
- The trait is defined with type parameter DonutDatabase[A]. If you are unsure about type parameters, please review the previous tutorial.
2. Create a class which extends trait DonutDatabase and knows how to perform CRUD operations with Apache Cassandra as storage layer
Let's assume that our storage layer is Apache Cassandra. We create a class called CassandraDonutStore[A] which will facilitate all the CRUD operations by extending trait DonutDatabase[A] from step 1.
println("\nStep 2: Create a class which extends trait DonutDatabase and knows how to perform CRUD operations with Apache Cassandra as storage layer")
class CassandraDonutStore[A] extends DonutDatabase[A] {
override def addOrUpdate(donut: A): Long = {
println(s"CassandraDonutDatabase-> addOrUpdate method -> donut: $donut")
1
}
override def query(donut: A): A = {
println(s"CassandraDonutDatabase-> query method -> donut: $donut")
donut
}
override def delete(donut: A): Boolean = {
println(s"CassandraDonutDatabase-> delete method -> donut: $donut")
true
}
}
3. Create a trait which will define the methods for a data access layer and will require dependency injection for DonutDatabase
Next, we create a trait called DonutShoppingCartDao which will be the main API to perform CRUD operations versus some storage layer. To this end, we define a val of type DonutDatabase[A] which will be injected as shown below.
println("\nStep 3: Create a trait which will define the methods for a data access layer and will require dependency injection for DonutDatabase")
trait DonutShoppingCartDao[A] {
val donutDatabase: DonutDatabase[A] // dependency injection
def add(donut: A): Long = {
println(s"DonutShoppingCartDao-> add method -> donut: $donut")
donutDatabase.addOrUpdate(donut)
}
def update(donut: A): Boolean = {
println(s"DonutShoppingCartDao-> update method -> donut: $donut")
donutDatabase.addOrUpdate(donut)
true
}
def search(donut: A): A = {
println(s"DonutShoppingCartDao-> search method -> donut: $donut")
donutDatabase.query(donut)
}
def delete(donut: A): Boolean = {
println(s"DonutShoppingCartDao-> delete method -> donut: $donut")
donutDatabase.delete(donut)
}
}
NOTE:
- By defining: val donutDatabase: DonutDatabase[A], we are not tying ourselves with any particular storage layer. Instead, the class DonutShoppingCartDao only mandates a type of DonutDatabase[A].
- In the previous example, we have only seen traits which had method signatures. Scala allows traits to also contain method implementations as shown above.
4. Create a trait which will define the methods for checking donut inventory and will require dependency injection for DonutDatabase
Similar to the previous examples, we create another trait which will be responsible to checking donut inventory.
println("\nStep 4: Create a trait which will define the methods for checking donut inventory and will require dependency injection for DonutDatabase")
trait DonutInventoryService[A] {
val donutDatabase: DonutDatabase[A] // dependency injection
def checkStockQuantity(donut: A): Int = {
println(s"DonutInventoryService-> checkStockQuantity method -> donut: $donut")
donutDatabase.query(donut)
1
}
}
NOTE:
- Trait DonutInventoryService[A] also mandates a type DonutDatabase: val donutDatabase: DonutDatabase[A]
5. Create a trait which will act as a facade and extends multiple traits namely trait DonutShoppingCartDao and trait DonutInventoryService.
We now create a facade which extends multiple traits namely trait DonutShoppingCartDao and trait DonutInventoryService. We also inject an implementation of our storage layer which in this case is an instance of CassandraDonutStore
println("\nStep 5: Create a trait which will act as a facade which extends multiple traits namely trait DonutShoppingCartDao and trait DonutInventoryService. It also inject the correct DonutDatabase implementation - a CassandraDonutStore")
trait DonutShoppingCartServices[A] extends DonutShoppingCartDao[A] with DonutInventoryService[A] {
override val donutDatabase: DonutDatabase[A] = new CassandraDonutStore[A]()
}
NOTE:
- We made use of the override val keywords.
6. Create a DonutShoppingCart class which extends a single facade named DonutShoppingCartServices to expose all the underlying features required by a DonutShoppingCart
With the facade DonutShoppingCartServices[A] defined in Step 5, we can now create a class DonutShoppingCart[A] which extends it.
println("\nStep 6: Create a DonutShoppingCart class which extends a single facade named DonutShoppingCartServices to expose all the underlying features required by a DonutShoppingCart")
class DonutShoppingCart[A] extends DonutShoppingCartServices[A] {
}
7. Create an instance of DonutShoppingCart and call the add, update, search and delete methods
You can now create an instance of DonutShoppingCart and call the add, update, search and delete methods which were inherited from trait DonutShoppingCartDao.
println("\nStep 7: Create an instance of DonutShoppingCart and call the add, update, search and delete methods")
val donutShoppingCart: DonutShoppingCart[String] = new DonutShoppingCart[String]()
donutShoppingCart.add("Vanilla Donut")
donutShoppingCart.update("Vanilla Donut")
donutShoppingCart.search("Vanilla Donut")
donutShoppingCart.delete("Vanilla Donut")
You should see the following output when you run your Scala application in IntelliJ:
Step 7: Create an instance of DonutShoppingCart and call the add, update, search and delete methods
DonutShoppingCartDao-> add method -> donut: Vanilla Donut
CassandraDonutDatabase-> addOrUpdate method -> donut: Vanilla Donut
DonutShoppingCartDao-> update method -> donut: Vanilla Donut
CassandraDonutDatabase-> addOrUpdate method -> donut: Vanilla Donut
DonutShoppingCartDao-> search method -> donut: Vanilla Donut
CassandraDonutDatabase-> query method -> donut: Vanilla Donut
DonutShoppingCartDao-> delete method -> donut: Vanilla Donut
CassandraDonutDatabase-> delete method -> donut: Vanilla Donut
8. Call the checkStockQuantity method
You can now call the checkStockQuantity() method which was inherited from trait DonutInventoryService.
println("\nStep 8: Call the checkStockQuantity method")
donutShoppingCart.checkStockQuantity("Vanilla Donut")
You should see the following output when you run your Scala application in IntelliJ:
Step 8: Call the checkStockQuantity method
DonutInventoryService-> checkStockQuantity method -> donut: Vanilla Donut
CassandraDonutDatabase-> query method -> donut: Vanilla Donut
Summary
In this tutorial, we went over the following:
- Create a trait which knows how to do create, read, update and delete operations CRUD to a given database
- Create a class which extends trait DonutDatabase and knows how to perform CRUD operations with Apache Cassandra as storage layer
- Create a trait which will define the methods for a data access layer and will require dependency injection for DonutDatabase
- Create a trait which will define the methods for checking donut inventory and will require dependency injection for DonutDatabase
- Create a trait which will act as a facade which extends multiple traits namely trait DonutShoppingCartDao and trait DonutInventoryService
- Create a DonutShoppingCart class which extends a single facade named DonutShoppingCartServices to expose all the underlying features required by a DonutShoppingCart
- Create an instance of DonutShoppingCart and call the add, update, search and delete methods
- Call the checkStockQuantity method
Tip
- We've kept the trait type parameters example simple but it would be good to review variance namely covariance and contra-variance type parameters.
- In upcoming tutorials in this Chapter, we will also show how to use traits to build some pure Functional Programming constructs such as Monoids and Functors and much more!
Source Code
The source code is available on the allaboutscala GitHub repository.
What's Next
In the next tutorial, I will go over Dependency Injection in further details and show how to avoid the pitfalls of the infamous Cake Pattern.