Wednesday, May 8, 2024

Mastering Go: Part 7 - Useful Design Patterns for Go

 Design Pattern

Design patterns are fundamental knowledge for a good application developer. A design pattern is a reusable solution for common problems. Below are some commonly used design patterns that help engineers improve productivity.

Dependency Injection

Dependency injection design pattern decouple the implementation of the code from external logic. The DI design pattern manages the dependencies between modules/components  in the software development. Dependent models will be loosely coupled using DI in Golang programming. 

We can inject dependencies using:
Constructor Injection: Dependencies are provided to a module/struct  through its constructor. When the instance of the struct is initialized,  the dependencies to the struct will be passed via its constructor. 

Example: continue  from the Part 6 of this series(Mastering Go: Part 6 - Source Code Management with GitHub.

Step 1.  Modify the module db_operation and add new file named db_repository.go in it. Write the below code in the db_repository.go file

package databaselayer

import (
"database/sql"

_ "github.com/lib/pq"
)

// Struct for database operation
type Repository struct {
dbOperation DatabaseOperations
}

// Constructor the read and prepare the DB connection string
func NewRepository(dbOperation DatabaseOperations) *Repository {
return &Repository{
dbOperation: dbOperation,
}
}

// Function to open the DB connection and execute the query
func (repo *Repository) ExecuteSelect(query string) (*sql.Rows, error) {
return repo.dbOperation.ExecuteSelect(query)
}


Step 2:  Modify db_operation.go file and add DatabaseOperations interface with method signature ExecuteSelect. Write below code in the file:
package databaselayer

import (
"database/sql"
"fmt"

_ "github.com/lib/pq"
)

// DatabaseOperations defines the methods for interacting with the database
type DatabaseOperations interface {
ExecuteSelect(query string) (*sql.Rows, error)
}

// Struct for database operation
type DatabaseOperation struct {
databaseConfig GetDatabaseConfig
}

// Constructor to read and prepare the DB connection string
func NewDatabaseOperation() *DatabaseOperation {
databaseConfig := NewGetDatabaseConfig()
return &DatabaseOperation{
databaseConfig: databaseConfig,
}
}

// ExecuteSelect opens a DB connection and executes the query
func (dbo *DatabaseOperation) ExecuteSelect(query string) (*sql.Rows, error) {
return dbo.executeSelect(query)
}

func (dbo *DatabaseOperation) getDbConnectionString() string {
return fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", dbo.databaseConfig.GetUser(), dbo.databaseConfig.GetPassword(), dbo.databaseConfig.GetDatabaseHost(), dbo.databaseConfig.GetConnectionPort(), dbo.databaseConfig.GetDatabaseName())
}

// Private Function to open the DB connection and execute the query
func (dbo *DatabaseOperation) executeSelect(query string) (*sql.Rows, error) {
var err error
// Open a connection to the database
db, err := sql.Open("postgres", dbo.getDbConnectionString())
if err != nil {
panic(err)
}
defer db.Close()

rows, err := db.Query(query)
if err != nil {
panic(err)
}
//defer rows.Close() // Close the rows when done
return rows, err
}


Step 3. Now, modify the address_formater.go file form the stringformater package to initialize the db_repository struct by injecting dbOperation interface. Write below code
package stringformater

import (
"fmt"
databaseLayer "learngo/db_operation" // import data formatter package
)

// Address struct
type Address struct {
HouseNumber string
StreetName string
City string
State string
ZipCode string
}

func (a *Address) Format() string {
return a.HouseNumber + " " + a.StreetName + ", " + a.City + ", " + a.State + " " + a.ZipCode
}

// This function call DB operation and return the collection of address
func GetAddress() ([]Address, error) {
dbOperation := databaseLayer.NewDatabaseOperation()

// initialize the repository injecting DatabaseOperations interface
repo := databaseLayer.NewRepository(dbOperation)

// Query to select data from the database table
query := `SELECT "HouseNumber", "StreetName", "City", "State", "ZipCode"
FROM learngo."Address"`

// Call the execute select DB operation
rows, err := repo.ExecuteSelect(query)
if err != nil {
return nil, err
}

// Initialize a slice to store Address structs
var addresses []Address

// Iterate over the rows
for rows.Next() {
var address Address
// Scan the values into variables
err := rows.Scan(&address.HouseNumber, &address.StreetName, &address.City, &address.State, &address.ZipCode)
if err != nil {
return nil, fmt.Errorf("error scanning row: %v", err)
}

// Append the scanned Person to the slice
addresses = append(addresses, address)
}

// Check for errors during iteration
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating over rows: %v", err)
}

// Return the list of Person structs
return addresses, nil
}


Carefully review the implementation of constructor injection used in the above code. The repository struct's constructor is initialized with injecting the DatabaseOperations interface. Later, the dbOperation is used to execute the DB operation.

Method Injection
Method injection is similar to constructor injection. Instead of injecting the dependencies during struct initialization, we inject dependencies while invoking a method.
Example: injecting DatabaseOperations interface via method parameter.
// Function to open the DB connection and execute the query
func (repo *Repository) ExecuteSelect(dbOperation DatabaseOperations, query string) (*sql.Rows, error) {
return dbOperation.ExecuteSelect(query)
}


Interface Injection
Mostly, we create structs and modules by implementing the interface. Therefore, interface injection can gather all the dependencies while implementing it.
Please, Refer to the Constructor injection example how the for interface DatabaseOperations injected to perform DB operation.

Concurrency Design pattern

Fan-in/Fan-out

When we run goroutines and execute the same task divided into multiple threads/subtasks, the concept of this pattern is to divide the task into smaller subsets and then aggregate the results. The fan-out allows multiple subroutines/functions to read data from the same channel until the channel is closed.

In contrast to the fan-out, the fan-in allows reading from multiple channels by a function/subroutine. In the fan-in stage of concurrent execution, all the output/channel data from multiple processes/subroutines will be combined/collected into a single result.
Example: Look into the main method, how the goruotines is using channel for concurrent processing
below code snippet from the main method:
//get person names
persons, err := stringformater.GetPersonNames()

if err != nil {
fmt.Println("Exception occured to get persons")
panic(err)
}

// Check if the address has values
if len(persons) > 0 {
// Loop through the collection and read properties
for _, person := range persons {
personName := &stringformater.PersonName{FirstName: person.FirstName, LastName: person.LastName}
go formatString(personName, ch)
formatedName, status := <-ch // receive formatted data from channel with channel status
channelStatus = status
fmt.Println(formatedName)
}
} else {
fmt.Println("No objects in the persons collection")
}

fmt.Println("Use name format package to format billing address!")

Let's take a closer look at how the concurrency patterns, fan-in and fan-out, worked in the above context:

Fan-out: In this example, the for loop initiates a goroutine for each value from the persons collection by calling the formatString function in a separate goroutine.

Fan-in: The main goroutine continues to execute while other concurrent goroutines are executing. After each concurrent goroutine completes its task (i.e., formatting a name), it sends the formatted data and channel status through the channel ch. The main goroutine then receives these formatted names from the channel (formattedName, status := <-ch).

Below are some other important design pattern to know.
  • Pipeline Pattern (concurrency design Pattern)
  • Worker Pool Pattern(Concurrency design pattern)
  • Empty Struct Pattern
  • Functional Options Pattern
  • Context Pattern
Code Repo: https://github.com/learnwithsharad/learngo/tree/sharad/dependencyInjection

Reference

https://go.dev/blog/pipelines
https://medium.com/@ansujain/dependency-injection-in-go-797689a8f89a

No comments:

Post a Comment

Mastering Go: Part 14 - Messaging with Apache Kafka(Go Implementation)

In this post, we will explore how to implement Apache Kafka messaging in Golang. Several packages are available, and the best choice depends...