avatarRamseyjiang

Summary

The article provides an in-depth explanation of the Multiton design pattern in Golang, including its concept, objectives, advantages, disadvantages, practical scenarios, and implementation with unit tests.

Abstract

The Multiton design pattern is a creational pattern that ensures a struct has a limited number of instances, each uniquely identified by a key. This pattern extends the Singleton pattern by allowing multiple instances, each with its own identity, and provides a global access point to these instances. The article discusses the objectives of the Multiton pattern, such as managing instances with unique identifiers, providing centralized access, and ensuring instance uniqueness per key. It also outlines the pattern's benefits, including controlled instance creation, efficient retrieval, and reduced need for multiple struct instances across the codebase. However, it notes potential drawbacks like increased complexity and memory usage, as well as concurrency issues if not correctly implemented. Real-world scenarios for the Multiton pattern in Golang include database connection pools, resource management, logger instances, load balancers, and caching mechanisms. The article concludes with two implementation examples, complete with unit tests, demonstrating how to apply the Multiton pattern in Golang for an ice cream instance and a database configuration manager.

Opinions

  • The Multiton pattern is considered beneficial for managing multiple instances of a struct that require unique identities.
  • The pattern is praised for its centralized control over instances, which can lead to more maintainable and efficient code.
  • The article suggests that the Multiton pattern can be particularly useful in scenarios where shared resources need to be managed efficiently, such as database connections and hardware resources.
  • The author acknowledges that while the Multiton pattern offers several advantages, it can introduce complexity

Multiton Design Pattern in Golang Two Examples with Unit Tests

In this article, I will explain the multiton design pattern’s concept, objectives, pros and cons, scenarios and how to implement, and provide two instances and unit tests.

Concept

The Multiton design pattern is a creational pattern that ensures a struct has only a limited number of instances, representing each instance by one of several possible keys. The key is used as an identifier to obtain a single instance from a registry of instances that are managed by the multiton.

The Multiton design pattern is similar to the Singleton pattern but with a twist. The Singleton pattern ensures that a struct has only one instance. In contrast, the Multiton pattern ensures that a struct has a limited number of instances, each with its own unique identifier. The Multiton pattern is helpful in situations where multiple instances of a struct are required, but each instance must have a unique identity.

Objectives

The objectives of the Multiton pattern are:

  1. Manage multiple instances of a struct, each with a unique identifier.
  2. Provide a global point of access to instances.
  3. Ensure that only one instance exists for each key.

Pros and Cons

Advantages of using the Multiton pattern:

  1. Enforces a single instance of a struct per key.
  2. Provides a centralized point of control for the instances.
  3. Supports efficient instance retrieval using the key.
  4. It avoids the need to create multiple instances of a struct in different parts of the code.

Disadvantages of using the Multiton pattern:

  1. Increases complexity compared to Singleton.
  2. This may lead to increased memory consumption for a large number of instances.
  3. It can be prone to concurrency issues if not implemented correctly.

Scenarios

Here are some real-world scenarios where you could use the Multiton pattern in Golang:

1. Database Connection Pool

When managing multiple connections to different databases, you can use the Multiton pattern to create a pool of connections for each database. Each pool is keyed by the database connection string or some other identifier, allowing efficient reuse of connections and preventing excessive creation of new connections.

2. Resource Management

When managing access to shared hardware resources, like printers, scanners, or IoT devices, the Multiton pattern can be used to create and manage separate instances for each resource. Instances are keyed by a unique identifier, such as a device ID, allowing for efficient allocation and controlled access to shared resources.

3. Logger Instances

When creating a logging system that handles logs from multiple modules or components, you can use the Multiton pattern to create separate logger instances for each module. Each logger instance is keyed by a unique identifier, such as the module name, ensuring that log messages are correctly segregated and can be filtered or processed independently.

4. Load Balancer

If your application relies on a set of backend services, you can use the Multiton pattern to implement a load balancer that maintains multiple instances of the backend services. Each instance can be identified by a unique key, and the load balancer can distribute the workload among the instances based on their capacity and availability.

5. Cache

The Multiton pattern can be used to implement caching mechanisms where you store frequently accessed data in memory for faster retrieval. Each cached object can be identified by a unique key, allowing you to efficiently access the data without the need for redundant queries or computations.

How to implement

  1. Define the Multiton struct with a private constructor and a private map to store instances.
  2. Create a mutex for thread-safe access to instances.
  3. Implement a function to get an instance of the Multiton struct based on a key.
  4. Use the GetInstance() function to retrieve instances of the Multiton struct.

The following image is how to implement the multiton pattern diagram. Perhaps you think about Golang does not have a class. Correct, Golang does not have classes in the traditional sense like some other object-oriented programming languages such as Java or C++. Instead, Go has types with methods, and it uses interfaces to achieve polymorphism. I use the term “class” as a general term to represent a type that has methods.

First instance

In the following instance, I will implement the ice cream instance. I will give a simple example first. The below code is the icecream.go file content. The parameter fruit in GetIceCream() method, is the key identifier for the multiton pattern. Here the GetIceCream() function is as the GetInstance().

package icecream

import (
   "fmt"
   "sync"
)

// Step 1: Define the Multiton struct with a private constructor
// and a private map to store instances.
type Multiton struct {
   Fruit  string
   Favour string
}

var (
   fruitMap = make(map[string]*Multiton)
   mutex    = &sync.Mutex{} // Step 2: Create a mutex for thread-safe access to instances.
)

// Step 3: Implement a function to get an instance of the Multiton struct based on a key.
func GetIceCream(fruit string) *Multiton {
   mutex.Lock()
   defer mutex.Unlock()

   if fruitMap[fruit] == nil {
      fruitMap[fruit] = &Multiton{
         Fruit:  fruit,
         Favour: fmt.Sprintf("Favour is %s", fruit),
      }
   }

   return fruitMap[fruit]
}

This implementation ensures that only one instance of the Multiton struct exists for each unique key and provides a global point of access to those instances.

The following code is the icecream_test.go file content.

package icecream

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestGetIceCream(t *testing.T) {
// Step 4: Use the GetIceCream() as the GetInstance() function 
// to retrieve instances of the Multiton struct.
   appleCream := GetIceCream("apple")
   berryCream := GetIceCream("strawberry")
   watermelonCream := GetIceCream("watermelon")

   assert.Equal(t, "Favour is apple", appleCream.Favour)
   assert.Equal(t, "Favour is strawberry", berryCream.Favour)
   assert.Equal(t, "Favour is watermelon", watermelonCream.Favour)
}

After these codes are finished, let’s run the unit tests. The tests result from the screenshot is below.

Second instance

In the below instance, I will implement the database config instance. The following code is the db.go file content. It is a little bit more complex than the first example. DBConfig represents a database configuration. ConfigManager represents a database configuration manager. ConfManager is a singleton instance of ConfigManager. GetConfig returns a database configuration for a given key. SetConfig sets a database configuration for a given key.

package dbconn

import (
   "fmt"
   "sync"
)

type DBConfig struct {
   Username string
   Password string
   Host     string
   Port     int
   DBName   string
}

type ConfigManager struct {
   configs map[string]*DBConfig
   once    sync.Once
}

var ConfManager ConfigManager

func (c *ConfigManager) GetConfig(key string) (*DBConfig, error) {
   c.once.Do(func() {
      c.configs = make(map[string]*DBConfig)
   })
   config, ok := c.configs[key]
   if !ok {
      return nil, fmt.Errorf("config for key '%s' not found", key)
   }
   return config, nil
}

func (c *ConfigManager) SetConfig(key string, config *DBConfig) {
   c.once.Do(func() {
      c.configs = make(map[string]*DBConfig)
   })
   c.configs[key] = config
}

In this example, we have a database configuration manager represented by the ConfigManager struct. The manager holds a map of database configurations, with each configuration identified by a unique key. The sync.Once type is used to ensure that the map of configurations is only created once, even if multiple goroutines attempt to access the map concurrently. The Instance variable is a singleton instance of ConfigManager, and the GetConfig and SetConfig methods allow for retrieving and setting database configurations in the map, respectively. Please notice in the first example, I use the sync.Mutex, in the second example, I use the sync.Once. Usages of them are different.

The below code is the db_test.go file content.

package dbconn

import (
   "fmt"
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestConfig(t *testing.T) {
   confTest := &DBConfig{
      Username: "user1",
      Password: "pass1",
      Host:     "localhost",
      Port:     5432,
      DBName:   "db1",
   }
   ConfManager.SetConfig("config test", confTest)

   configDev := &DBConfig{
      Username: "user2",
      Password: "pass2",
      Host:     "localhost",
      Port:     5433,
      DBName:   "db2",
   }
   ConfManager.SetConfig("config dev", configDev)

   config, err := ConfManager.GetConfig("config test")
   if err != nil {
      fmt.Println(err)
      return
   }
   assert.Equal(t, confTest.Username, config.Username)
   assert.Equal(t, confTest.Port, config.Port)

   config, err = ConfManager.GetConfig("config dev")
   if err != nil {
      fmt.Println(err)
      return
   }
   assert.Equal(t, configDev.Username, config.Username)
   assert.Equal(t, configDev.Port, config.Port)
}

After these codes are finished, let’s run the unit tests. The tests result from the screenshot is below.

Conclusion

In conclusion, the Multiton pattern aims to solve the problem of managing multiple instances of a struct while ensuring that each instance is unique and can be easily accessed using a key. Implementing a multiton ensures that instances are created only when needed and reused whenever possible, leading to a more efficient and maintainable codebase.

Go back to Creational Design Patterns click here.

To View Structural Design Patterns in Golang, please click here.

To View Behavioural Design Patterns in Golang, please click here.

To View Concurrency Design Patterns in Golang, please click here.

Design Patterns
Software Engineering
Programming
Golang
Technology
Recommended from ReadMedium