Skip to main content

Abstract Factory Pattern

The Abstract Factory pattern provides a way to create related objects without specifying their concrete classes. This is useful in situations where we want to create objects based on a certain theme or context. Let's explore how the Abstract Factory pattern can help us implement a UI themes feature in a clean, extensible way.

The Problem: Implementing UI Themes Without a Pattern

First, let's look at a naive implementation without using the Abstract Factory pattern:

package main

import "fmt"

type Button struct {
backgroundColor string
textColor string
}

type Window struct {
backgroundColor string
}

func createLightThemeButton() *Button {
return &Button{
backgroundColor: "white",
textColor: "black",
}
}

func createDarkThemeButton() *Button {
return &Button{
backgroundColor: "black",
textColor: "white",
}
}

func createLightThemeWindow() *Window {
return &Window{
backgroundColor: "light-gray",
}
}

func createDarkThemeWindow() *Window {
return &Window{
backgroundColor: "dark-gray",
}
}

func main() {
// Create light theme UI
lightButton := createLightThemeButton()
lightWindow := createLightThemeWindow()
fmt.Printf("Light Theme - Button: %+v, Window: %+v\n", lightButton, lightWindow)

// Create dark theme UI
darkButton := createDarkThemeButton()
darkWindow := createDarkThemeWindow()
fmt.Printf("Dark Theme - Button: %+v, Window: %+v\n", darkButton, darkWindow)
}

This approach works, but it has several drawbacks:

  1. Violation of Open-Closed Principle (OCP): If we want to add a new theme (e.g., system), we need to modify existing code, adding new functions for each UI element.
  2. Lack of Cohesion: The theme-specific logic is spread across multiple functions, making it harder to maintain and update consistently.
  3. Difficult to Extend: Adding new UI elements (e.g., a Dropdown) requires adding new functions for each theme.
  4. No Guarantee of Consistency: There's no mechanism ensuring that all UI elements for a theme are created together, potentially leading to mixed themes.

The Solution: Abstract Factory Pattern

Now, let's refactor this using the Abstract Factory pattern:

package main

import "fmt"

// UIFactory is our abstract factory interface
type UIFactory interface {
CreateButton() Button
CreateWindow() Window
}

// Button interface
type Button interface {
Render() string
}

// Window interface
type Window interface {
Render() string
}

// LightThemeFactory concrete factory
type LightThemeFactory struct{}

func (f LightThemeFactory) CreateButton() Button {
return LightButton{}
}

func (f LightThemeFactory) CreateWindow() Window {
return LightWindow{}
}

// DarkThemeFactory concrete factory
type DarkThemeFactory struct{}

func (f DarkThemeFactory) CreateButton() Button {
return DarkButton{}
}

func (f DarkThemeFactory) CreateWindow() Window {
return DarkWindow{}
}

// Light theme concrete products
type LightButton struct{}

func (b LightButton) Render() string {
return "Rendering light theme button"
}

type LightWindow struct{}

func (w LightWindow) Render() string {
return "Rendering light theme window"
}

// Dark theme concrete products
type DarkButton struct{}

func (b DarkButton) Render() string {
return "Rendering dark theme button"
}

type DarkWindow struct{}

func (w DarkWindow) Render() string {
return "Rendering dark theme window"
}

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

func main() {
lightFactory := LightThemeFactory{}
darkFactory := DarkThemeFactory{}

fmt.Println("Creating Light Theme UI:")
createUI(lightFactory)

fmt.Println("\nCreating Dark Theme UI:")
createUI(darkFactory)
}

Benefits of the Abstract Factory Pattern

  1. Adheres to Open-Closed Principle (OCP): We can add new themes (e.g., SystemThemeFactory) without modifying existing code. The system is open for extension but closed for modification.
  2. Ensures Consistency: All UI elements for a theme are created by the same factory, guaranteeing a consistent look.
  3. Easy to Extend: Adding new UI elements (e.g., Dropdown) only requires updating the UIFactory interface and concrete factories, not the client code.
  4. Separation of Concerns: Theme-specific logic is encapsulated within each concrete factory and product, making the code more organized and maintainable.
  5. Flexibility: Client code (createUI function) works with abstractions, making it easy to switch themes or even allow runtime theme switching.

Conclusion

The Abstract Factory pattern provides a robust solution for creating families of related objects, such as UI elements with consistent theming. By using this pattern, we've created a system that's easier to maintain, extend, and keeps our code adherent to important design principles like the Open-Closed Principle.

This approach not only solves our immediate problem of supporting multiple UI themes but also sets us up for future extensibility. Whether we need to add new themes, new UI elements, or even support dynamic theme switching, our Abstract Factory implementation provides a solid foundation for these enhancements.