Design Patterns in Go

repository·main·Indexed 18 days ago

https://github.com/refactoringguru/design-patterns-go

A collection of Go implementations for all classic Gang of Four (GoF) design patterns, providing a practical reference for implementing these patterns using idiomatic Go code. Requires Go v1.19 or higher.

Tokens
28K
Snippets
150
Records
166
Agent score
63%

What's inside refactoringguru-design-patterns-go

  1. Run the SyncOnce singleton example

    main

    The main package in singleton/syncOnce/main.go demonstrates a thread-safe Singleton pattern implementation by launching 30 concurrent goroutines that all attempt to call getInstance(). This simulates a high-concurrency environment to verify that the singleton instance is initialized only once.

    To run this specific example, navigate to the directory and use the standard Go run command.

    go run singleton/syncOnce/main.go
  2. Use the Iterator pattern to traverse a UserCollection

    main

    The Iterator pattern allows you to traverse elements of a collection without exposing its underlying representation. In this implementation, you can create an iterator from a UserCollection and use hasNext() and getNext() to loop through the users.

    To use it:

    1. Initialize a UserCollection with a slice of *User pointers.
    2. Call createIterator() on the collection to get an iterator instance.
    3. Use a for loop with iterator.hasNext() to check for more elements and iterator.getNext() to retrieve the current element.
    user1 := &User{
    	name: "a",
    	age:  30,
    }
    user2 := &User{
    	name: "b",
    	age:  20,
    }
    
    userCollection := &UserCollection{
    	users: []*User{user1, user2},
    }
    
    iterator := userCollection.createIterator()
    
    for iterator.hasNext() {
    	user := iterator.getNext()
    	fmt.Printf("User is %+v\n", user)
    }
  3. Implement the Memento pattern in Go

    main

    The Memento pattern allows you to capture and restore an object's internal state without violating encapsulation. This implementation uses three main components:

    1. Originator: The object whose state you want to save. It provides methods to create a Memento containing its current state and to restore its state from a Memento.
    2. Memento: A value object that stores the internal state of the Originator.
    3. Caretaker: Responsible for keeping track of multiple Mementos (e.g., in a history list) but never operates on or examines the contents of a Memento.

    To use this pattern, initialize a Caretaker to manage history, an Originator with an initial state, and use originator.createMemento() to save snapshots and originator.restoreMemento(memento) to roll back.

    package main
    
    import "fmt"
    
    func main() {
    	// Initialize the Caretaker to manage history
    	caretaker := &Caretaker{
    		mementoArray: make([]*Memento, 0),
    	}
    
    	// Initialize the Originator with an initial state
    	originator := &Originator{
    		state: "A",
    	}
    
    	fmt.Printf("Originator Current State: %s\n", originator.getState())
    	// Save current state
    	caretaker.addMemento(originator.createMemento())
    
    	originator.setState("B")
    	fmt.Printf("Originator Current State: %s\n", originator.getState())
    	caretaker.addMemento(originator.createMemento())
    
    	originator.setState("C")
    	fmt.Printf("Originator Current State: %s\n", originator.getState())
    	caretaker.addMemento(originator.createMemento())
    
    	// Restore to a previous state using the Caretaker's history
    	originator.restoreMemento(caretaker.getMemento(1))
    	fmt.Printf("Restored to State: %s\n", originator.getState())
    
    	originator.restoreMemento(caretaker.getMemento(0))
    	fmt.Printf("Restored to State: %s\n", originator.getState())
    }
  4. Implement the Chain of Responsibility pattern

    main

    The Chain of Responsibility pattern allows you to pass a request along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.

    In this implementation, a Patient request is passed through a chain of departments: Reception -> Doctor -> Medical -> Cashier. Each department implements an execute method to handle the patient.

    package main
    
    func main() {
    	cashier := &Cashier{}
    
    	// Set next for medical department
    	medical := &Medical{}
    	medical.setNext(cashier)
    
    	// Set next for doctor department
    	doctor := &Doctor{}
    	doctor.setNext(medical)
    
    	// Set next for reception department
    	reception := &Reception{}
    	reception.setNext(doctor)
    
    	patient := &Patient{name: "abc"}
    	// Patient visiting
    	reception.execute(patient)
    }
  5. Run the Mediator pattern example

    main

    The main package provides a demonstration of the Mediator pattern. It simulates a train station environment where a stationManager (the mediator) coordinates the movement of PassengerTrain and FreightTrain objects. This prevents the trains from communicating directly with each other, instead routing all interactions through the mediator.

    package main
    
    func main() {
    	stationManager := newStationManger()
    
    	passengerTrain := &PassengerTrain{
    		mediator: stationManager,
    	}
    	freightTrain := &FreightTrain{
    		mediator: stationManager,
    	}
    
    	passengerTrain.arrive()
    	freightTrain.arrive()
    	passengerTrain.depart()
    }
  6. Use the Client in the Adapter pattern

    main

    In the Adapter pattern implementation, the Client represents the high-level component that interacts with the Computer interface. The client uses the InsertLightningConnectorIntoComputer method to perform operations on a Computer instance, which abstracts away the specific connector/port compatibility logic.

    type Client struct {
    }
    
    func (c *Client) InsertLightningConnectorIntoComputer(com Computer) {
    	fmt.Println("Client inserts Lightning connector into computer.")
    	com.InsertIntoLightningPort()
    }
  7. Run the Observer pattern example

    main

    The observer/main.go file serves as an entrypoint to demonstrate the Observer design pattern. It simulates a scenario where a shirtItem (the Subject) notifies multiple Customer instances (the Observers) when its availability is updated.

    package main
    
    func main() {
    	shirtItem := newItem("Nike Shirt")
    
    	observerFirst := &Customer{id: "abc@gmail.com"}
    	observerSecond := &Customer{id: "xyz@gmail.com"}
    
    	shirtItem.register(observerFirst)
    	shirtItem.register(observerSecond)
    
    	shirtItem.updateAvailability()
    }
  8. Run the Proxy pattern example

    main

    The proxy/main.go file serves as a demonstration of the Proxy design pattern. It simulates an Nginx server acting as a proxy that handles HTTP requests for specific URLs like /app/status and /create/user.

    To run this specific implementation, you can execute the main package. The example demonstrates how the proxy intercepts requests, handles different HTTP methods (GET, POST), and returns an HTTP status code along with a response body.

    package main
    
    import "fmt"
    
    func main() {
    	nginxServer := newNginxServer()
    	appStatusURL := "/app/status"
    	createuserURL := "/create/user"
    
    	httpCode, body := nginxServer.handleRequest(appStatusURL, "GET")
    	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
    
    	httpCode, body = nginxServer.handleRequest(appStatusURL, "GET")
    	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
    
    	httpCode, body = nginxServer.handleRequest(appStatusURL, "GET")
    	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
    
    	httpCode, body = nginxServer.handleRequest(createuserURL, "POST")
    	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
    
    	httpCode, body = nginxServer.handleRequest(createuserURL, "GET")
    	fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
    }
  9. Use the Strategy pattern to switch cache eviction algorithms

    main

    The Strategy pattern implementation allows you to dynamically change the eviction algorithm used by a cache at runtime.

    To use it:

    1. Initialize a cache with a starting strategy (e.g., Lfu, Lru, or Fifo) using initCache(strategy).
    2. Add items to the cache using cache.add(key, value).
    3. Switch the eviction algorithm at any time using cache.setEvictionAlgo(newStrategy).

    Available strategies demonstrated in this implementation include:

    • Lfu (Least Frequently Used)
    • Lru (Least Recently Used)
    • Fifo (First In, First Out)
    // Initialize cache with LFU strategy
    lfu := &Lfu{}
    cache := initCache(lfu)
    
    cache.add("a", "1")
    
    // Switch to LRU strategy at runtime
    lru := &Lru{}
    cache.setEvictionAlgo(lru)
    
    cache.add("d", "4")
    
    // Switch to FIFO strategy at runtime
    fifo := &Fifo{}
    cache.setEvictionAlgo(fifo)
    
    cache.add("e", "5")
  10. Use the Adapter pattern to connect incompatible interfaces

    main

    The Adapter pattern allows incompatible interfaces to work together. In this implementation, a Client expects a LightningConnector compatible interface (represented by the Mac type), but can interact with a Windows machine by using a WindowsAdapter. The adapter wraps the incompatible Windows object and exposes the methods required by the Client.

    package main
    
    func main() {
    	// Scenario 1: Direct compatibility
    	client := &Client{}
    	mac := &Mac{}
    	client.InsertLightningConnectorIntoComputer(mac)
    
    	// Scenario 2: Using an Adapter for incompatible types
    	windowsMachine := &Windows{}
    	windowsMachineAdapter := &WindowsAdapter{
    		windowMachine: windowsMachine,
    	}
    	client.InsertLightningConnectorIntoComputer(windowsMachineAdapter)
    }