HardТеория4 min

Abstract Factory

Абстрактная фабрика в Go: создание семейств связанных объектов через интерфейсы

Abstract Factory (Абстрактная фабрика)

Проблема

Вам нужно создавать семейства связанных объектов, которые должны быть совместимы друг с другом. Например, UI-элементы для разных ОС: кнопка и чекбокс для macOS должны выглядеть в одном стиле, а для Linux -- в другом.

Abstract Factory гарантирует, что клиент получает совместимые объекты, не зная их конкретных типов.

Диаграмма паттерна

    +------------------+       +------------------+
    |   UIFactory      |       |    Button        | <-- interface
    +------------------+       +------------------+
    | CreateButton()   |       | Render()         |
    | CreateCheckbox() |       +------------------+
    | CreateInput()    |              ^
    +------------------+         +----+----+
            ^                    |         |
     +------+------+        MacBtn    LinuxBtn
     |             |
  MacFactory   LinuxFactory
                                +------------------+
                                |   Checkbox       | <-- interface
                                +------------------+
                                | Render()         |
                                | Toggle()         |
                                +------------------+
                                       ^
                                  +----+----+
                                  |         |
                               MacCB    LinuxCB

Реализация

Определение интерфейсов продуктов

package ui

// Button represents a clickable UI element.
type Button interface {
    Render() string
    OnClick(handler func())
}

// Checkbox represents a togglable UI element.
type Checkbox interface {
    Render() string
    Toggle()
    IsChecked() bool
}

// Input represents a text input UI element.
type Input interface {
    Render() string
    SetValue(v string)
    Value() string
}

Определение интерфейса фабрики

// Factory creates a family of related UI components.
type Factory interface {
    CreateButton(label string) Button
    CreateCheckbox(label string) Checkbox
    CreateInput(placeholder string) Input
}

macOS реализация

package ui

import "fmt"

// --- macOS Button ---

type macButton struct {
    label   string
    onClick func()
}

func (b *macButton) Render() string {
    return fmt.Sprintf("[macOS Button: %s]", b.label)
}

func (b *macButton) OnClick(handler func()) {
    b.onClick = handler
}

// --- macOS Checkbox ---

type macCheckbox struct {
    label   string
    checked bool
}

func (c *macCheckbox) Render() string {
    mark := " "
    if c.checked {
        mark = "x"
    }
    return fmt.Sprintf("[macOS (%s) %s]", mark, c.label)
}

func (c *macCheckbox) Toggle() {
    c.checked = !c.checked
}

func (c *macCheckbox) IsChecked() bool {
    return c.checked
}

// --- macOS Input ---

type macInput struct {
    placeholder string
    value       string
}

func (i *macInput) Render() string {
    if i.value == "" {
        return fmt.Sprintf("[macOS Input: %s]", i.placeholder)
    }
    return fmt.Sprintf("[macOS Input: %s]", i.value)
}

func (i *macInput) SetValue(v string) { i.value = v }
func (i *macInput) Value() string     { return i.value }

// --- macOS Factory ---

type macFactory struct{}

// NewMacFactory creates a UI factory for macOS components.
func NewMacFactory() Factory {
    return &macFactory{}
}

func (f *macFactory) CreateButton(label string) Button {
    return &macButton{label: label}
}

func (f *macFactory) CreateCheckbox(label string) Checkbox {
    return &macCheckbox{label: label}
}

func (f *macFactory) CreateInput(placeholder string) Input {
    return &macInput{placeholder: placeholder}
}

Linux реализация

package ui

import "fmt"

type linuxButton struct {
    label   string
    onClick func()
}

func (b *linuxButton) Render() string {
    return fmt.Sprintf("< GTK Button: %s >", b.label)
}

func (b *linuxButton) OnClick(handler func()) {
    b.onClick = handler
}

type linuxCheckbox struct {
    label   string
    checked bool
}

func (c *linuxCheckbox) Render() string {
    mark := " "
    if c.checked {
        mark = "*"
    }
    return fmt.Sprintf("[ GTK [%s] %s ]", mark, c.label)
}

func (c *linuxCheckbox) Toggle()       { c.checked = !c.checked }
func (c *linuxCheckbox) IsChecked() bool { return c.checked }

type linuxInput struct {
    placeholder string
    value       string
}

func (i *linuxInput) Render() string {
    if i.value == "" {
        return fmt.Sprintf("| GTK Input: %s |", i.placeholder)
    }
    return fmt.Sprintf("| GTK Input: %s |", i.value)
}

func (i *linuxInput) SetValue(v string) { i.value = v }
func (i *linuxInput) Value() string     { return i.value }

type linuxFactory struct{}

// NewLinuxFactory creates a UI factory for Linux/GTK components.
func NewLinuxFactory() Factory {
    return &linuxFactory{}
}

func (f *linuxFactory) CreateButton(label string) Button {
    return &linuxButton{label: label}
}

func (f *linuxFactory) CreateCheckbox(label string) Checkbox {
    return &linuxCheckbox{label: label}
}

func (f *linuxFactory) CreateInput(placeholder string) Input {
    return &linuxInput{placeholder: placeholder}
}

Выбор фабрики по конфигурации

package ui

import (
    "fmt"
    "runtime"
)

// NewFactory returns a UI factory for the current operating system.
func NewFactory() (Factory, error) {
    return NewFactoryForOS(runtime.GOOS)
}

// NewFactoryForOS returns a UI factory for the specified OS.
func NewFactoryForOS(os string) (Factory, error) {
    switch os {
    case "darwin":
        return NewMacFactory(), nil
    case "linux":
        return NewLinuxFactory(), nil
    default:
        return nil, fmt.Errorf("unsupported OS: %s", os)
    }
}

Клиентский код

func RenderLoginForm(factory ui.Factory) {
    // Client works with abstract interfaces only.
    // It doesn't know if it's macOS or Linux.
    username := factory.CreateInput("Username")
    password := factory.CreateInput("Password")
    remember := factory.CreateCheckbox("Remember me")
    submit := factory.CreateButton("Log In")

    fmt.Println(username.Render())
    fmt.Println(password.Render())
    fmt.Println(remember.Render())
    fmt.Println(submit.Render())
}

func main() {
    factory, err := ui.NewFactory()
    if err != nil {
        log.Fatal(err)
    }
    RenderLoginForm(factory)
}

Реальный пример: драйвер базы данных

package db

import "context"

// Row represents a single database row.
type Row interface {
    Scan(dest ...any) error
}

// Rows represents a result set.
type Rows interface {
    Next() bool
    Scan(dest ...any) error
    Close() error
}

// Conn represents a database connection.
type Conn interface {
    QueryRow(ctx context.Context, query string, args ...any) Row
    Query(ctx context.Context, query string, args ...any) (Rows, error)
    Exec(ctx context.Context, query string, args ...any) (int64, error)
    Close() error
}

// TxManager represents transaction support.
type TxManager interface {
    Begin(ctx context.Context) (Tx, error)
}

// Tx represents a database transaction.
type Tx interface {
    Conn
    Commit() error
    Rollback() error
}

// Factory creates database-specific implementations.
type Factory interface {
    NewConn(ctx context.Context, dsn string) (Conn, error)
    NewTxManager(conn Conn) TxManager
}

Когда использовать

Используйте, когда:

  • Система должна работать с несколькими семействами связанных объектов
  • Нужно гарантировать совместимость объектов одного семейства
  • Плагинная система с набором связанных компонентов
  • Поддержка разных платформ/протоколов/баз данных

Не используйте, когда:

  • Есть только одно семейство объектов (достаточно Factory Method)
  • Объекты семейства не связаны между собой
  • Количество типов продуктов часто меняется (сложно расширять интерфейс фабрики)

Сравнение с похожими паттернами

Паттерн Отличие
Factory Method Создаёт один тип объекта; Abstract Factory -- семейство
Builder Пошаговое создание одного сложного объекта
Prototype Клонирование вместо создания с нуля

Проверь себя

В чём главное отличие Abstract Factory от Factory Method?

Какая проблема возникает при добавлении нового типа продукта в Abstract Factory?

Как клиентский код выбирает конкретную фабрику в Go?