Design Pattern Series - Abstract Factory Pattern (Golang)
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 macOSWindow
). - 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 aRender()
method to display the button’s UI. - The
Window
interface requires aDraw()
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 theButton
interface by providing theRender()
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 theButton
interface with macOS-specific rendering behavior. - The difference in behavior between
WindowsButton
andMacOSButton
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 theWindow
interface with theDraw()
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 theWindow
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()
andCreateWindow()
, which return objects implementing theButton
andWindow
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 theUIFactory
interface. - The
CreateButton()
method returns aWindowsButton
, andCreateWindow()
returns aWindowsWindow
. - 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
implementsUIFactory
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 aUIFactory
parameter and uses it to createButton
andWindow
objects. - Client code doesn’t need to know the concrete classes, only calling the
Render()
andDraw()
methods. - In
main
, we test with bothWindowsFactory
andMacOSFactory
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 withWindowsFactory
, 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!