Taking Advantage of the Adapter Design Pattern
We have discussed about the Adapter Design Pattern in Go and we wrapped the Redigo library to illustrate the concept. During my research I discovered that Redigo has a strong competitor called Go-Redis. I spent sometime playing with it and my first impression was that the code became more concise with Go-Redis. It is also better documented. I didn’t compare their performance, but if we find out that Go-Redis is better than Redigo, what would be the overall impact of switching to Go-Redis?
Well, not much. Since we are using the adapter pattern to hide the caching mechanism from the rest of the code, we know that only a delimited part of the code is assigned to deal with Redis. This part is the struct that implements our Cache interface. But before starting the changes, we need to cover the existing code with unit tests. Here is a sample of the unit tests written for each method of our Cache interface:
package main
import (
"testing"
)
func TestRedisCache_Get(t *testing.T) {
cache := GetCachingMechanism()
cache.Put("single", "Single Record")
if "Single Record" != cache.Get("single") {
t.Fail()
}
}
func TestRedisCache_Put(t *testing.T) {
cache := GetCachingMechanism()
cache.Put("single", "Single Record")
if "Single Record" != cache.Get("single") {
t.Fail()
}
}
...
The complete set of tests is available in the blog’s repo. With the tests, we want to ensure that the new code with Go-Redis still works like the one with Redigo. It also illustrates how to use the Cache interface, so if we don’t change the tests after moving to Go-Redis then the Adapter Pattern has served its purpose.
The following code is the Go-Redis implementation:
type RedisCache struct {
conn *redis.Client
ctx context.Context
}
// GetCachingMechanism initializes and returns a caching mechanism.
func GetCachingMechanism() Cache {
cch := &RedisCache{
conn: redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
}),
}
cch.ctx = context.Background()
return cch
}
// Put adds an entry in the cache.
func (rc *RedisCache) Put(key string, value interface{}) {
if err := rc.conn.Set(rc.ctx, key, value, 0); err != nil {
fmt.Println(err)
}
}
// PutAll adds the entries of a map in the cache.
func (rc *RedisCache) PutAll(entries map[string]interface{}) {
for k, v := range entries {
rc.Put(k, v)
}
}
// Get gets an entry from the cache.
func (rc *RedisCache) Get(key string) interface{} {
value, err := rc.conn.Get(rc.ctx, key).Result()
if err != nil {
fmt.Println(err)
return ""
}
return value
}
// GetAll gets all the entries of a map from the cache.
func (rc *RedisCache) GetAll(keys []string) map[string]interface{} {
entries := make(map[string]interface{})
for _, k := range keys {
entries[k] = rc.Get(k)
}
return entries
}
// Clean cleans a entry from the cache.
func (rc *RedisCache) Clean(key string) {
if err := rc.conn.Del(rc.ctx, key); err != nil {
fmt.Println(err)
}
}
// CleanAll cleans the entire cache.
func (rc *RedisCache) CleanAll() {
rc.conn.FlushDB(rc.ctx)
}
Comparing to the Redigo implementation, we only changed the body of the methods and kept the signatures intact. A complete example of this code is available in the blog’s repo. To check whether everything is still working, I run the tests:
$ cd blog-examples/caching
$ go test
All unit tests pass without changes. It means the application can gracefully evolve over time with the freedom to upgrade existing libraries or move to better ones all together without concerns. Of course, moving to another library requires you to perform more tests including integration and performance, but doing it without further code changes is a huge gain.
Recent Posts
Can We Trust Marathon Pacers?
Introducing LibRunner
Clojure Books in the Toronto Public Library

Once Upon a Time in Russia

FHIR: A Standard For Healthcare Data Interoperability

First Release of CSVSource

Astonishing Carl Sagan's Predictions Published in 1995

Making a Configurable Go App

Dealing With Pressure Outside of the Workplace

Reacting to File Changes Using the Observer Design Pattern in Go

Provisioning Azure Functions Using Terraform

Applying The Adapter Design Pattern To Decouple Libraries From Go Apps

Using Goroutines to Search Prices in Parallel

Applying the Strategy Pattern to Get Prices from Different Sources in Go
