MidТеория3 min

Factory Method

Фабричный метод в Go: функции-конструкторы NewXxx, интерфейсные фабрики и реальные примеры

Factory Method (Фабричный метод)

Проблема

Вам нужно создавать объекты, не привязываясь к конкретным типам. Клиентский код не должен знать, какой именно тип будет создан -- он работает через интерфейс.

В классических ООП-языках это решается через наследование и виртуальные методы. В Go всё гораздо проще -- у нас есть интерфейсы и функции-конструкторы.

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

                    +-------------------+
                    |    Storage        |  <-- interface
                    +-------------------+
                    | Save(data []byte) |
                    | Load(key string)  |
                    +-------------------+
                           ^
            +--------------+--------------+
            |              |              |
    +-------+------+ +----+-------+ +----+-------+
    | FileStorage  | | S3Storage  | | MemStorage |
    +--------------+ +------------+ +------------+

    NewStorage(typ string) Storage   <-- factory function

Реализация: Функция-конструктор NewXxx

Самый распространённый паттерн в Go -- функция NewXxx, которая возвращает интерфейс или конкретный тип.

package storage

import (
    "fmt"
    "io"
    "os"
    "sync"
)

// Storage defines the contract for data persistence.
type Storage interface {
    Save(key string, data []byte) error
    Load(key string) ([]byte, error)
    Delete(key string) error
}

// --- File storage implementation ---

type fileStorage struct {
    basePath string
}

func newFileStorage(basePath string) *fileStorage {
    return &fileStorage{basePath: basePath}
}

func (fs *fileStorage) Save(key string, data []byte) error {
    path := fs.basePath + "/" + key
    return os.WriteFile(path, data, 0644)
}

func (fs *fileStorage) Load(key string) ([]byte, error) {
    path := fs.basePath + "/" + key
    return os.ReadFile(path)
}

func (fs *fileStorage) Delete(key string) error {
    path := fs.basePath + "/" + key
    return os.Remove(path)
}

// --- In-memory storage implementation ---

type memStorage struct {
    mu   sync.RWMutex
    data map[string][]byte
}

func newMemStorage() *memStorage {
    return &memStorage{
        data: make(map[string][]byte),
    }
}

func (ms *memStorage) Save(key string, data []byte) error {
    ms.mu.Lock()
    defer ms.mu.Unlock()
    ms.data[key] = append([]byte(nil), data...) // defensive copy
    return nil
}

func (ms *memStorage) Load(key string) ([]byte, error) {
    ms.mu.RLock()
    defer ms.mu.RUnlock()
    d, ok := ms.data[key]
    if !ok {
        return nil, fmt.Errorf("key %q not found", key)
    }
    return append([]byte(nil), d...), nil // defensive copy
}

func (ms *memStorage) Delete(key string) error {
    ms.mu.Lock()
    defer ms.mu.Unlock()
    delete(ms.data, key)
    return nil
}

Фабричная функция

// NewStorage creates a Storage based on the given type.
// This is the factory method -- the client doesn't know
// which concrete type is returned.
func NewStorage(typ string, opts ...string) (Storage, error) {
    switch typ {
    case "file":
        if len(opts) == 0 {
            return nil, fmt.Errorf("file storage requires base path")
        }
        return newFileStorage(opts[0]), nil
    case "memory":
        return newMemStorage(), nil
    default:
        return nil, fmt.Errorf("unknown storage type: %q", typ)
    }
}

Реализация: Фабрика через реестр

Более гибкий подход -- реестр фабрик. Позволяет регистрировать новые типы без изменения фабричной функции.

package storage

import (
    "fmt"
    "sync"
)

// Factory is a function that creates a Storage instance.
type Factory func(opts map[string]string) (Storage, error)

// Registry holds registered storage factories.
type Registry struct {
    mu        sync.RWMutex
    factories map[string]Factory
}

// NewRegistry creates an empty factory registry.
func NewRegistry() *Registry {
    return &Registry{
        factories: make(map[string]Factory),
    }
}

// Register adds a factory for the given storage type.
func (r *Registry) Register(name string, f Factory) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.factories[name] = f
}

// Create instantiates a Storage by name.
func (r *Registry) Create(name string, opts map[string]string) (Storage, error) {
    r.mu.RLock()
    f, ok := r.factories[name]
    r.mu.RUnlock()
    if !ok {
        return nil, fmt.Errorf("unknown storage type: %q", name)
    }
    return f(opts)
}

Использование реестра:

func main() {
    reg := storage.NewRegistry()

    // Register factories
    reg.Register("file", func(opts map[string]string) (storage.Storage, error) {
        path, ok := opts["path"]
        if !ok {
            return nil, fmt.Errorf("file storage: missing path option")
        }
        return storage.NewFileStorage(path), nil
    })

    reg.Register("memory", func(opts map[string]string) (storage.Storage, error) {
        return storage.NewMemStorage(), nil
    })

    // Create storage without knowing the concrete type
    s, err := reg.Create("memory", nil)
    if err != nil {
        log.Fatal(err)
    }

    _ = s.Save("greeting", []byte("Hello, Go!"))
}

Go vs Java: сравнение фабрик

Аспект Java Go
Механизм Абстрактный класс + наследование Интерфейс + функция
Сложность Высокая (abstract class, extends) Низкая (interface + NewXxx)
Гибкость Через наследование Через композицию
Типичный вид StorageFactory.create() NewStorage() или storage.New()
Реестр Map<String, Factory> map[string]Factory

В Go нет необходимости в абстрактных классах -- интерфейсы и функции первого класса решают задачу проще и чище.

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

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

  • Нужно создавать объекты без привязки к конкретному типу
  • Тип объекта определяется конфигурацией или параметром
  • Нужен реестр для расширения через плагины
  • Тестирование требует подмены реализаций

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

  • Есть только одна реализация (просто используйте New())
  • Тип известен на этапе компиляции
  • Создание объекта тривиально

Реальные примеры в стандартной библиотеке

// sql.Open -- фабрика для database/sql
db, err := sql.Open("postgres", connStr)

// image.Decode -- фабрика для декодеров изображений
img, format, err := image.Decode(reader)

// crypto.Hash -- реестр хеш-функций
h := crypto.SHA256.New()

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

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

Проверь себя

Какой самый идиоматичный способ реализации Factory Method в Go?

Какая функция стандартной библиотеки Go является примером Factory Method?

Какой тип имеет функция Factory в реестре фабрик?