Design Pattern Series - Abstract Factory Pattern (Golang)

Go Design Pattern Series May 27, 2025

Previous post in the series: Factory Pattern

What is the Abstract Factory Pattern?

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. Instead of directly creating objects, this pattern uses a factory to generate objects belonging to a specific family.

For example, if you're building a UI application for different operating systems (Windows, macOS), the Abstract Factory Pattern allows you to create UI components like Button or Window tailored to each operating system without modifying the main logic code.

Key Characteristics:

  • Abstracts Object Creation: Client code works with interfaces rather than concrete classes, reducing dependencies.
  • Creates Related Object Groups: Each factory produces a set of objects from the same family, ensuring consistency.
  • Flexible: Easily switch or extend object families at runtime without modifying client code.

Benefits and Use Cases

Benefits:

  • Ensures Consistency: Guarantees that created objects belong to the same family, avoiding mismatched components (e.g., using a Windows Button with a macOS Window).
  • Increases Abstraction: Client code interacts only with interfaces, making it easier to maintain, extend, and reduce dependency on concrete classes.
  • Reduces Complexity: Clients don’t need to know the details of object creation, only the appropriate factory to use.

Use Cases:

  • Building Cross-Platform UIs: Creating UI components (e.g., Button, Window, Scrollbar) for operating systems like Windows, macOS, or Linux.
  • Systems with Multiple Variants: When you need to create related product groups, such as car parts (Engine, Wheel) for different car types (sedan, SUV).
  • Dynamic Configuration Changes: When you need to switch entire object families at runtime without modifying code.

Example Implementation in Go

We will implement the Abstract Factory Pattern in Go using a UI system example. This example creates Button and Window components for two operating systems: Windows and macOS. The code is split into separate files for clarity, with detailed explanations for each step.

Step 1: Define Interfaces for Products

We start by defining interfaces for the abstract products (Button and Window). These interfaces specify the methods that every concrete product must implement.

// products.go
package main

type Button interface {
    Render() string
}

type Window interface {
    Draw() string
}

Explanation:

  • The Button interface requires a Render() method to display the button’s UI.
  • The Window interface requires a Draw() method to render the window.
  • These interfaces act as contracts that concrete products (e.g., WindowsButton, MacOSWindow) must follow.
  • Using interfaces ensures that client code depends only on abstract behaviors, not concrete classes.

Step 2: Implement Concrete Products

Next, we create concrete structs to implement the above interfaces, representing specific products for each operating system.

Windows Button:

// windows_button.go
package main

type WindowsButton struct{}

func (b WindowsButton) Render() string {
    return "Rendering a Windows button"
}

Explanation:

  • The WindowsButton struct implements the Button interface by providing the Render() method.
  • The Render() method returns a string describing the rendering behavior of a button on Windows, e.g., "Rendering a Windows button".

macOS Button:

// macos_button.go
package main

type MacOSButton struct{}

func (b MacOSButton) Render() string {
    return "Rendering a macOS button"
}

Explanation:

  • Similarly, MacOSButton implements the Button interface with macOS-specific rendering behavior.
  • The difference in behavior between WindowsButton and MacOSButton reflects the diversity of families.

Windows Window:

// windows_window.go
package main

type WindowsWindow struct{}

func (w WindowsWindow) Draw() string {
    return "Drawing a Windows window"
}

Explanation:

  • The WindowsWindow struct implements the Window interface with the Draw() method.
  • This method describes the rendering behavior of a window in the Windows style.

macOS Window:

// macos_window.go
package main

type MacOSWindow struct{}

func (w MacOSWindow) Draw() string {
    return "Drawing a macOS window"
}

Explanation:

  • MacOSWindow implements the Window interface with macOS-specific window rendering behavior.
  • These structs ensure that each family (Windows or macOS) has compatible products.

Step 3: Define the Abstract Factory Interface

We define a UIFactory interface to specify methods for creating products (Button and Window).

// ui_factory.go
package main

type UIFactory interface {
    CreateButton() Button
    CreateWindow() Window
}

Explanation:

  • The UIFactory interface defines two methods: CreateButton() and CreateWindow(), which return objects implementing the Button and Window interfaces, respectively.
  • This interface is the core of the Abstract Factory Pattern, allowing concrete factories to implement product creation in their own way.
  • Client code uses UIFactory to create objects without knowing the specific concrete classes.

Step 4: Implement Concrete Factories

We create concrete factories (WindowsFactory and MacOSFactory) to produce products for the Windows or macOS families.

Windows Factory:

// windows_factory.go
package main

type WindowsFactory struct{}

func (f WindowsFactory) CreateButton() Button {
    return WindowsButton{}
}

func (f WindowsFactory) CreateWindow() Window {
    return WindowsWindow{}
}

Explanation:

  • The WindowsFactory struct implements the UIFactory interface.
  • The CreateButton() method returns a WindowsButton, and CreateWindow() returns a WindowsWindow.
  • This ensures that all products created belong to the Windows family.

macOS Factory:

// macos_factory.go
package main

type MacOSFactory struct{}

func (f MacOSFactory) CreateButton() Button {
    return MacOSButton{}
}

func (f MacOSFactory) CreateWindow() Window {
    return MacOSWindow{}
}

Explanation:

  • Similarly, MacOSFactory implements UIFactory and creates products for the macOS family.
  • Each factory is responsible for creating all products in its family, ensuring consistency.

Step 5: Use the Abstract Factory in Client Code

The client code uses UIFactory to create objects without knowing their specific family.

// main.go
package main

import "fmt"

func createUI(factory UIFactory) {
    button := factory.CreateButton()
    window := factory.CreateWindow()
    fmt.Println(button.Render())
    fmt.Println(window.Draw())
}

func main() {
    // Create UI for Windows
    windowsFactory := WindowsFactory{}
    createUI(windowsFactory)

    // Create UI for macOS
    macOSFactory := MacOSFactory{}
    createUI(macOSFactory)
}

Explanation:

  • The createUI function takes a UIFactory parameter and uses it to create Button and Window objects.
  • Client code doesn’t need to know the concrete classes, only calling the Render() and Draw() methods.
  • In main, we test with both WindowsFactory and MacOSFactory to create corresponding UIs.
  • This approach allows client code to switch between families (Windows or macOS) without changing its logic.

Program Output

When running the program, the output will be:

Rendering a Windows button
Drawing a Windows window
Rendering a macOS button
Drawing a macOS window

Explanation of Output:

  • When createUI is called with WindowsFactory, products from the Windows family are created and displayed.
  • When called with MacOSFactory, products from the macOS family are created and displayed.
  • This demonstrates the flexibility of the Abstract Factory Pattern: client code remains unchanged when switching between families.

Next post in this series: Singleton Pattern:

Thanks for reading!

Tags