One of the most resourceful implementations of design patterns. I tried to use more words to make it more verbose, so that anyone can pick the concepts. But primarily languages used are Golang
, Python
- SOLID Principles
- UML
- Creational
- Structural
- Behavioural
- Microservices
- Others
- AntiPatterns
- Resources
- Credits
Creates an instance of several families of classes.
Separates object construction from its representation.
- Create an instance of several derived classes.
- Define an interface for creating an object, but let subclasses decide which class to instantiate.
- Factory Method lets a class defer instantiation to subclasses.
- Make all implemented constructors private or protected.
- Avoid expensive acquisition and release of resources by recycling objects.
- Significant Performance Boost
- Used where
- cost of initializing a class instance is high,
- the rate of instantiation of a class is high,
- the number of instantiation in use at any one time is low.
- Object Caching
- A.K.A resource pool
- the pool will be a growing pool.
- we can restricts the number of objects created.
- It is desirable to keep all Reusable objects that are not currently in use in the same object pool so that they can be managed by one coherent policy. To achieve this, the Reusable Pool class is designed to be a singleton class.
- we don't want a process to have to wait for a particular object to be released, so the Object Pool also instantiates new objects as they are required, but must also implement a facility to clean up unused objects periodically.
Credits - SourceMaking
type Reusable struct{}
type ReusablePool struct {
Objects []Reusable
MaxPoolSize int
}
func (r *ReusablePool) Acquire() Reusable{
if r.Objects == nil {
r.Objects = make([]Reusable, MaxPoolSize)
}
r.Objects = r.Objects[1:]
}
func (r *ReusablePool) Release(re *Reusable) {
}
func main() {
reusablePool := &ReusablePool{MaxPoolSize: 10}
reusable := reusablePool.Acquire()
reusablePool.Release(reusable)
}
The Prototype Pattern creates duplicate objects while keeping performance in mind.
- It requires implementing a prototype interface which tells to create a clone of the current object.
- It is used when creation of object directly is costly.
For instance, an object is to be created after a costly database operation. We can cache the object, returns its clone on next request and update the database as and when needed thus reducing the database calls.
Example - 1: generate different configuration files depending on our needs
package configurer
type Config struct {
workDir string
user string
}
func NewConfig(user string, workDir string) Config {
return Config{
user: user,
workDir: workDir,
}
}
func (c Config) WithUser(user string) Config {
c.user = user
return c
}
func (c Config) WithWorkDir(workDir string) Config {
c.workDir = workDir
return c
}
We want to be able to mutate the object without affecting its initial instance. The goal is to be able to generate different configuration files without loosing the flexibility of customizing them without mutation of the initial default configuration.
- only one instance
- global point to access the instance
- initialization on first use
If app needs one and only one instance of an object.
type privateStructure struct {
value string
}
var singleVariable privateStructure
func GetSingletonInstance() privateStructure {
if singleVariable != nil {
return singleVariable
}
singleVariable = privateStructure{
value: "some data",
}
return singleVariable
}
A thread-safe solution might be
var mu sync.Mutex
func GetInstance() *singleton {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
return instance
}
Check-Lock-Check
Pattern
func GetInstance() *singleton {
if instance == nil {
mu.Lok()
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
}
return instance
}
But using the sync/atomic package, we can atomically load and set a flag that will indicate if we have initialized or not our instance.
import sync
import sync/atomic
var initialized uint32
func Getinstance() *singleton{
if atomic.LoadUInt32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &singleton{}
atomic.StoreUint32(&initialized, 1)
}
}
Idiomatic singleton approach in go
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func(){
instance = &singleton
})
return instance
}
Example Code
package main
import (
"fmt"
"github.com/alamin-mahamud/go-design-patterns/singleton"
)
func main() {
s := singleton.GetInstance()
s.Data = 1
fmt.Println("1st -> ", s.Data)
s2 := singleton.GetInstance()
fmt.Println("2nd -> ", s2.Data)
s3 := singleton.GetInstance()
fmt.Println("3rd -> ", s3.Data)
s2.Data = 20
fmt.Println("1st -> ", s.Data)
fmt.Println("2nd -> ", s2.Data)
fmt.Println("3rd -> ", s3.Data)
s3.Data = 10
fmt.Println("1st -> ", s.Data)
fmt.Println("2nd -> ", s2.Data)
fmt.Println("3rd -> ", s3.Data)
}
///////////////////////////////////////////
// 1st -> 1
// 2nd -> 1
// 3rd -> 1
// 1st -> 20
// 2nd -> 20
// 3rd -> 20
// 1st -> 10
// 2nd -> 10
// 3rd -> 10
////////////////////////////////////////////
Ease the design by identifying a simple way to realize relationships between entities.
Match interfaces of different classes.
We can use type-c adapter
for using our traditional usb-2
, usb-3
, micro-SD
, ethernet
devices. They are not same. We used to have different ports for these devices.
But this adapter let's us work with common ground.
type RowingBoat interface{
row()
}
type FishingBoat struct {}
func (f *FishingBoat) sail() {}
type Captain struct {}
func (c *Captain) row(){}
type FishingBoatAdapter struct {
fishingBoat FishingBoat
}
func (f *FishingBoatAdapter) row() {
boat.sail()
}
type TwoPinCharger interface {
TwoPinCharge()
}
type TwoPinChargerMobile struct {}
func (t *TwoPinChargerMobile) TwoPinCharge() {}
type ThreePinCharger interface {
ThreePinCharge()
}
func (t *ThreePinChargerMobile) ThreePinCharge() {}
type TwoPin2ThreePinChargerAdapter struct {
twoPinCharger TwoPinCharger
}
func (t *TwoPin2ThreePinChargerAdapter) ThreePinCharge() {
twoPinCharger.TwoPinCharge()
}
// TODO: fix implementation using Class Diagram
// To use the adapter
twoPinCharger := &TwoPinChargerMobile{}
adapter := &TwoPin2ThreePinChargeAdapter{twoPinCharger}
adapter.ThreePinCharge()
Separate an object's interface from it's implementation
A tree structure of simple and composite objects.
Add responsibilities to objects dynamically.
A single class that represents an entire subsystem.
A fine-grained instance used for efficient sharing.
Restricts accessor/mutator access.
An object representing another object.
A way of passing a request to a chain of objects.
Encapsulates a command request as an object
A way to include language elements in a program.
Sequentially access the elements of a collection.
Defines simplified communication between classes.
Capture and restore an object's internal state.
Designed to act as a default value of an object.
A way of notifying change to a number of classes.
Alter an object's behaviour when it's state change.
Encapsulates an algorithm inside a class.
Defer the exact steps of an algorithm to a subclass.
Defines a new operation to a class without change.
[Placeholder ...]
[Placeholder ...]
Add Property on the fly.
Allow new functions to be added to existing class hierarchies without affecting those hierarchies, and without creating the troublesome dependency cycles that are inherent to the GOF VISITOR
Pattern.
// TODO: implementaion
Commonly occuring solutions to a problem, that generates decidedly negative consequences.
When Manager or Developer apply good design patterns into wrong context.
Antipatterns provide a detailed plan to reverse the situation.
- Software Development Antipatterns
- Software Architecture Antipatterns
- Software Product Management Antipatterns