A deep dive into the critical aspects of becoming an effective technical leader and architect Introduction Venkat Subramaniam, a renowned software developer, consultant, and author, shares valuable insights into becoming an effective technical leader and architect in a recent The […]
Design pattern
Distributing SQL Databases Globally
I hope you enjoyed learning the article. Stay tuned! Subscribe to the new letter and The GeekNarrator youtube channel.
Cheers,
The GeekNarrator
Is Monolith Architecture Bad? Are Microservices Always Good? Conversation with Anant.
In this conversation we talk to Ananth about his experience with Microservices and Monlithic architectures. We also look at various examples of a good/bad monolith, good/bad micro services. We also talk about our personal experience with different architectural styles and […]
What is Decorator Pattern?
This pattern lets a programmer add more functionality to an object without impacting other objects of the class. So off course without inheritance
Why do you need it?
At times we need our objects to have more functionality based on different use cases. One way to achieve this is via Inheritance, but it is not always possible and it impacts all objects of the class, which might not be suitable.
Let’s build something in Scala to understand why we need it and how we can use Scala Traits to achieve it.
DB
trait represents a Key,Value Database with simple functions for fetching, updating and deleting data.
trait DB { def getData(k: String): String def putData(k: String, v: String): Unit def deleteData(k: String): Unit }
We have a SimpleDB
which implements this trait. It holds key -> value
pairs in memory using a simple mutable
map.
class SimpleDB extends DB { private val dbData = scala.collection.mutable .Map[String, String]() override def putData(key: String, value: String): Unit = { dbData.put(key, value) } override def deleteData(key: String): Unit = { dbData.remove(key) } override def getData(id: String): String = { dbData.get(id).getOrElse("no value found") } }
We would like to separate clients based on their access rights. Some clients may be read-only
access, some may also have write-access
and administrators can have delete-access
as well.
Let’s define some traits for different types of access.
Provides read access only
trait ReadOnly { val db: DB def getData(id: String): String = db.getData(id) }
Provides additional write access
trait Writer extends ReadOnly {
val db: DB
def deleteData(key: String) = db.deleteData(key)
def putData(key: String, value: String) = db.putData(key, value)
}
So now we have a simple database and some interfaces to access data based on access rights.
Let’s define simple clients to access our Database.
class DBClient(override val db: DB) extends ReadOnly
By default all clients will have a read-only
access.
Now how do we create clients with write access as well?
We have two options:
- Create a new client and extend
Writer
trait.- This is ok if you have only two such cases, but what if you have more flavors ? Will you create new clients for each one?
- On the same client extend
Writer
trait as well.- This means there won’t be any read-only client all will have write access as well.
- Also sometimes you won’t have control on changing the class definition, may be because its a third party library?
Third option goes something like this:
//Create a db val simpleDB = new SimpleDB //A read only client val readOnlyClient = new DBClient(simpleDB)
when we create a new client (read-only) using a with
keyword you can add additional feature of other Traits.
val writeableClient = new DBClient(simpleDB) with Writer
What this means is that only this object of DBClient
will have the write-access
without impacting any other object.
Just to have another example lets say to improve the performance of read calls we want to add caching functionality to our clients. How do we do that? Another trait and then mix it up when we need it.
Caching trait can add caching functionality to any client. mutable.Map
‘s getOrElseUpdate
takes a key and function to fetch the value if not already present in the cache. Here super.getData
will call ReadOnly clients getData
function
//Trait which adds a caching functionality trait Caching extends ReadOnly { //caches data val data = scala.collection.mutable.Map[String, String]() override def getData(key: String): String = { data.getOrElseUpdate(key, super.getData(key)) } }
Create a caching client as follows :
val cachingClient = new DBClient(simpleDB) with Caching
Test our clients:
readOnlyClient.getData("1") writeableClient.putData("3", "value of 3") readOnlyClient.getData("3") cachingClient.getData("3") writeableClient.deleteData("3") readOnlyClient.getData("3")
Note : This is just a simple implementation to demonstrate Scala mixin as Decorator pattern. The caching and database implementations are far from ideal and in real world we will have more sophisticated libraries to access database and implement caching. But it can still be taken as a general pattern to wrap underlying clients to add or hide functionality. Cheers, Kaivalya