Design patterns are well-established solutions to common software design problems. Structural design patterns focus on how objects and classes are composed to form larger structures while ensuring flexibility and efficiency.
Common Structural Design Patterns
1. Adapter Pattern
The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two interfaces, converting one interface into another that a client expects.
Why Use the Adapter Pattern?
Interface Compatibility – When you have a legacy system with an interface that does not match a new system.
Third-party Library Integration – When using third-party APIs that don’t fit your existing codebase.
Code Reusability – Allows old classes or methods to work with new implementations without modifying existing code.
Implementation in Go
package main
import "fmt"
// 1. Define the Target Interface
// This is the interface expected by the new system.
type Printer interface {
Print(msg string)
}
// 2. Implement the Legacy System
// The existing LegacyPrinter has a different method signature.
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintLegacy(msg string) {
fmt.Println("Legacy Printer Output: " + msg)
}
// 3. Create the Adapter
// The PrinterAdapter wraps around LegacyPrinter and implements the Printer interface.
type PrinterAdapter struct {
legacyPrinter *LegacyPrinter
}
func (pa *PrinterAdapter) Print(msg string) {
if pa.legacyPrinter == nil {
fmt.Println("Error: Legacy printer is not initialized")
return
}
fmt.Println("Adapter in Action:")
pa.legacyPrinter.PrintLegacy(msg)
}
// 4. Use the Adapter in the Client Code
// Now, you can use PrinterAdapter to allow your new system to interact with LegacyPrinter.
func main() {
legacyPrinter := &LegacyPrinter{}
adapter := &PrinterAdapter{legacyPrinter: legacyPrinter}
// Using the adapter to call Print() which internally calls PrintLegacy()
adapter.Print("Hello, World!")
}
2. Bridge Pattern
The Bridge Pattern is a structural design pattern that separates abstraction from implementation, allowing them to evolve independently. It is useful when you want to avoid a permanent binding between an abstraction and its implementation.
Why Use the Bridge Pattern?
Decouples abstraction and implementation – Changes in one do not affect the other.
Improves code scalability – Useful when dealing with multiple implementations.
Enhances flexibility – New implementations can be added without modifying existing code.
Implementation in Go
Imagine we are designing a remote control system that works with different types of devices (e.g., TVs and Radios).
The remote control is an abstraction.
The device (TV or Radio) is the implementation.
The Bridge Pattern allows us to change remote control logic independently of device logic.
package main
import "fmt"
// Device is the implementation interface that different devices (TV, Radio) must implement.
type Device interface {
TurnOn()
TurnOff()
}
// TV is a concrete implementation of the Device interface.
type TV struct{}
func (t *TV) TurnOn() {
fmt.Println("TV is now ON")
}
func (t *TV) TurnOff() {
fmt.Println("TV is now OFF")
}
// Radio is another concrete implementation of the Device interface.
type Radio struct{}
func (r *Radio) TurnOn() {
fmt.Println("Radio is now ON")
}
func (r *Radio) TurnOff() {
fmt.Println("Radio is now OFF")
}
// Remote is the abstraction that delegates commands to the Device implementation.
type Remote struct {
device Device // The bridge connecting abstraction to implementation
}
func (r *Remote) TogglePower() {
fmt.Println("Toggling power...")
r.device.TurnOn() // Calls TurnOn() of the associated device
}
// AdvancedRemote extends Remote with additional functionality.
type AdvancedRemote struct {
remote *Remote
}
func (ar *AdvancedRemote) Mute() {
fmt.Println("Muting the device")
}
// Main function demonstrating the Bridge Pattern.
func main() {
tv := &TV{} // Creating a TV instance
radio := &Radio{} // Creating a Radio instance
tvRemote := &Remote{device: tv} // Bridge pattern in action
radioRemote := &Remote{device: radio}
tvRemote.TogglePower() // Turns on TV
radioRemote.TogglePower() // Turns on Radio
advancedRemote := &AdvancedRemote{remote: tvRemote}
advancedRemote.Mute() // Mutes the TV
}
3. Composite Pattern
The Composite Pattern is a structural design pattern that allows you to treat individual objects and compositions of objects uniformly. This pattern is particularly useful when dealing with tree-like structures, such as file systems, UI hierarchies, or organizational charts.
Why Use the Composite Pattern?
Uniformity – Treats both single objects and groups of objects the same way.
Hierarchical Structure – Perfect for tree-like object compositions.
Scalability – Easily adds new object types without modifying existing code.
Implementation in Go:
package main
import "fmt"
// Component interface defines the common behavior for both files and folders.
type Component interface {
ShowDetails(indent string) // Displays the structure
}
// File is a leaf node (cannot have children).
type File struct {
name string
}
// ShowDetails prints the file name with indentation.
func (f *File) ShowDetails(indent string) {
fmt.Println(indent + "- File:", f.name)
}
// Folder is a composite node (can contain files or other folders).
type Folder struct {
name string
children []Component // Holds both files and folders
}
// Add allows adding a file or folder to the folder.
func (f *Folder) Add(component Component) {
f.children = append(f.children, component)
}
// ShowDetails prints the folder name and recursively prints its children.
func (f *Folder) ShowDetails(indent string) {
fmt.Println(indent + "+ Folder:", f.name)
for _, child := range f.children {
child.ShowDetails(indent + " ") // Indentation for hierarchy
}
}
// Main function demonstrating the Composite Pattern.
func main() {
// Creating files
file1 := &File{name: "file1.txt"}
file2 := &File{name: "file2.txt"}
file3 := &File{name: "file3.txt"}
// Creating folders
folder1 := &Folder{name: "Documents"}
folder2 := &Folder{name: "Pictures"}
rootFolder := &Folder{name: "Root"}
// Building the hierarchy
folder1.Add(file1)
folder1.Add(file2)
folder2.Add(file3)
rootFolder.Add(folder1)
rootFolder.Add(folder2)
// Displaying the file system structure
rootFolder.ShowDetails("")
}
Execution Output:
+ Folder: Root
+ Folder: Documents
- File: file1.txt
- File: file2.txt
+ Folder: Pictures
- File: file3.txt
4. Decorator Pattern
The Decorator Pattern is a structural design pattern that allows you to dynamically add new behavior to an object without modifying its existing code. This is useful when you want to extend functionalities at runtime instead of modifying the base class or interface directly.
Why Use the Decorator Pattern?
Open-Closed Principle – You can extend functionality without modifying existing code.
Flexible Enhancements – Add/remove features at runtime.
Reduces Code Duplication – Avoids subclass explosion by using composition instead.
Implementation in Go:
package main
import "fmt"
// Component interface (Base coffee type)
type Coffee interface {
Cost() int
Description() string
}
// Concrete component (Plain coffee)
type SimpleCoffee struct{}
func (s *SimpleCoffee) Cost() int {
return 5 // Base price
}
func (s *SimpleCoffee) Description() string {
return "Simple Coffee"
}
// Decorator interface (Wraps a Coffee)
type CoffeeDecorator struct {
coffee Coffee
}
func (c *CoffeeDecorator) Cost() int {
return c.coffee.Cost()
}
func (c *CoffeeDecorator) Description() string {
return c.coffee.Description() + ", Coffee"
}
// Concrete decorator (Adds Milk)
type MilkDecorator struct {
coffee Coffee
}
func (m *MilkDecorator) Cost() int {
return m.coffee.Cost() + 2 // Adds $2 for milk
}
func (m *MilkDecorator) Description() string {
return m.coffee.Description() + ", Milk"
}
// Concrete decorator (Adds Sugar)
type SugarDecorator struct {
coffee Coffee
}
func (s *SugarDecorator) Cost() int {
return s.coffee.Cost() + 1 // Adds $1 for sugar
}
func (s *SugarDecorator) Description() string {
return s.coffee.Description() + ", Sugar"
}
// Main function demonstrating the Decorator Pattern.
func main() {
coffee := &SimpleCoffee{} // Base coffee
fmt.Println(coffee.Description(), "=> $", coffee.Cost())
// Add Milk
coffeeWithMilk := &MilkDecorator{coffee: coffee}
fmt.Println(coffeeWithMilk.Description(), "=> $", coffeeWithMilk.Cost())
// Add Sugar
coffeeWithSugar := &SugarDecorator{coffee: coffee}
fmt.Println(coffeeWithSugar.Description(), "=> $", coffeeWithSugar.Cost())
// Add Both Milk and Sugar
coffeeWithMilkAndSugar := &SugarDecorator{coffee: coffeeWithMilk}
fmt.Println(coffeeWithMilkAndSugar.Description(), "=> $", coffeeWithMilkAndSugar.Cost())
}
5. Facade Pattern
The Facade Pattern is a structural design pattern that provides a simplified, high-level interface to a complex system. Instead of exposing multiple subsystems, Facade creates a single entry point that simplifies interactions.
Why Use the Facade Pattern?
Simplifies complex subsystems – Provides a unified, easy-to-use interface.
Reduces dependencies – Clients only interact with the Facade instead of multiple subsystems.
Encapsulates changes – Any changes in subsystems do not affect clients using the Facade.
Improves maintainability – Clean, modular, and organised code.
Implementation in Go:
package main
import "fmt"
// Subsystem 1: DVD Player
type DVDPlayer struct{}
func (d *DVDPlayer) On() {
fmt.Println("DVD Player is ON")
}
func (d *DVDPlayer) Play(movie string) {
fmt.Println("Playing movie:", movie)
}
func (d *DVDPlayer) Off() {
fmt.Println("DVD Player is OFF")
}
// Subsystem 2: Projector
type Projector struct{}
func (p *Projector) On() {
fmt.Println("Projector is ON")
}
func (p *Projector) SetInput(source string) {
fmt.Println("Projector input set to:", source)
}
func (p *Projector) Off() {
fmt.Println("Projector is OFF")
}
// Subsystem 3: Sound System
type SoundSystem struct{}
func (s *SoundSystem) On() {
fmt.Println("Sound System is ON")
}
func (s *SoundSystem) SetVolume(level int) {
fmt.Println("Sound System volume set to:", level)
}
func (s *SoundSystem) Off() {
fmt.Println("Sound System is OFF")
}
// Facade: Home Theater System
type HomeTheaterFacade struct {
dvdPlayer *DVDPlayer
projector *Projector
soundSystem *SoundSystem
}
func NewHomeTheaterFacade() *HomeTheaterFacade {
return &HomeTheaterFacade{
dvdPlayer: &DVDPlayer{},
projector: &Projector{},
soundSystem: &SoundSystem{},
}
}
// Simplified interface to start a movie
func (h *HomeTheaterFacade) WatchMovie(movie string) {
fmt.Println("\nStarting Home Theater...")
h.dvdPlayer.On()
h.dvdPlayer.Play(movie)
h.projector.On()
h.projector.SetInput("DVD Player")
h.soundSystem.On()
h.soundSystem.SetVolume(10)
fmt.Println("Enjoy your movie!\n")
}
// Simplified interface to stop a movie
func (h *HomeTheaterFacade) EndMovie() {
fmt.Println("\nShutting down Home Theater...")
h.dvdPlayer.Off()
h.projector.Off()
h.soundSystem.Off()
fmt.Println("Goodbye!\n")
}
// Main function demonstrating the Facade Pattern
func main() {
homeTheater := NewHomeTheaterFacade()
// Using a simple method instead of calling subsystems separately
homeTheater.WatchMovie("Inception")
homeTheater.EndMovie()
}
6. Flyweight Pattern
The Flyweight Pattern is a structural design pattern that helps reduce memory usage by sharing common objects instead of creating multiple duplicate instances. It is particularly useful when dealing with a large number of similar objects.
Why Use the Flyweight Pattern?
Reduces memory usage – Shares common data instead of duplicating it.
Improves performance – Reduces object creation overhead.
Encapsulates intrinsic & extrinsic states – Separates shared and unique data.
Best for large-scale object management – Useful when many objects have the same properties.
Implementation in Go:
Imagine a game where you need to render thousands of trees.
Instead of creating a new object for every tree, we can reuse shared tree data (like type and color) while storing only unique details (like position).
Intrinsic State (Shared Data): Tree type, Color, Texture
Extrinsic State (Unique Data): X & Y Coordinates
package main
import (
"fmt"
)
// Flyweight interface (Defines common behavior)
type Tree interface {
Display(x, y int)
}
// Concrete Flyweight (Shared tree properties)
type TreeType struct {
name string // Intrinsic state (Shared)
color string // Intrinsic state (Shared)
}
func (t *TreeType) Display(x, y int) {
fmt.Printf("Tree: %s, Color: %s at Position (%d, %d)\n", t.name, t.color, x, y)
}
// Flyweight Factory (Manages shared tree objects)
type TreeFactory struct {
treeTypes map[string]*TreeType
}
// NewTreeFactory creates a factory for managing shared trees
func NewTreeFactory() *TreeFactory {
return &TreeFactory{treeTypes: make(map[string]*TreeType)}
}
// GetTreeType returns a shared TreeType object
func (f *TreeFactory) GetTreeType(name, color string) *TreeType {
key := name + "-" + color // Unique key for each tree type
if _, exists := f.treeTypes[key]; !exists {
fmt.Println("Creating new tree type:", name, "with color:", color)
f.treeTypes[key] = &TreeType{name: name, color: color}
}
return f.treeTypes[key]
}
// Client code to demonstrate Flyweight pattern usage
func main() {
factory := NewTreeFactory()
// Using shared tree types instead of creating new ones
tree1 := factory.GetTreeType("Oak", "Green")
tree1.Display(10, 20)
tree2 := factory.GetTreeType("Oak", "Green") // Reuses existing Oak-Green tree type
tree2.Display(30, 40)
tree3 := factory.GetTreeType("Pine", "Dark Green") // Creates new Pine-Dark Green tree type
tree3.Display(50, 60)
tree4 := factory.GetTreeType("Oak", "Green") // Reuses existing Oak-Green tree type
tree4.Display(70, 80)
}
7. Proxy Pattern
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It acts as an intermediary between the client and the real object, adding functionalities such as lazy initialization, access control, logging, and caching.
Why Use the Proxy Pattern?
Access Control – Restricts access to sensitive objects (e.g., authentication).
Lazy Initialization – Creates heavy objects only when needed.
Logging & Monitoring – Logs requests before passing them to the real object.
Performance Optimization – Caches results to avoid redundant operations.
Security – Hides complex or sensitive objects from direct access.
Implementation in Go:
Imagine a company firewall that restricts access to certain websites.
A proxy server acts as an intermediary that filters access before allowing or blocking a request.
Real Object: Direct internet access
Proxy Object: A firewall proxy that restricts certain websites
Client: Employees trying to access the internet
package main
import (
"fmt"
)
// Subject Interface (Defines common behavior)
type Internet interface {
ConnectTo(website string)
}
// Real Object (Actual internet access)
type RealInternet struct{}
func (r *RealInternet) ConnectTo(website string) {
fmt.Println("Connected to", website)
}
// Proxy Object (Controls access)
type ProxyInternet struct {
realInternet *RealInternet
blockedSites map[string]bool
}
// NewProxyInternet initializes proxy with blocked websites
func NewProxyInternet() *ProxyInternet {
return &ProxyInternet{
realInternet: &RealInternet{},
blockedSites: map[string]bool{
"blocked.com": true,
"malware.com": true,
},
}
}
// ConnectTo method adds access control
func (p *ProxyInternet) ConnectTo(website string) {
if p.blockedSites[website] {
fmt.Println("Access Denied to", website)
return
}
p.realInternet.ConnectTo(website)
}
// Client code demonstrating Proxy Pattern
func main() {
internet := NewProxyInternet()
// Allowed websites
internet.ConnectTo("example.com")
internet.ConnectTo("google.com")
// Blocked websites
internet.ConnectTo("blocked.com")
internet.ConnectTo("malware.com")
}
Conclusion
The Structural Design Patterns in Go, including Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy, help in organizing code, improving scalability, and optimizing performance. Each pattern serves a distinct purpose:
Adapter bridges incompatible interfaces.
Bridge separates abstraction from implementation.
Composite structures objects into trees.
Decorator dynamically extends functionality.
Facade simplifies complex subsystems.
Flyweight optimizes memory by sharing objects.
Proxy controls access and adds security.
By leveraging these patterns, developers can write cleaner, reusable, and more maintainable code. Understanding when and how to apply them ensures efficient, scalable, and high-performance software design. 🚀
Thank you for following along through this journey. I hope you found it helpful and informative. Feel free to reach out to Gyanesh Sharma