Блог инженера

History is written by its contributors

Factory Pattern в Go: создание объектов с помощью фабрик

2025-06-10 время чтения 5 мин Patterns Ilya Brin

Factory Pattern решает одну проблему: как создавать объекты, не привязываясь к конкретным типам. Вместо прямого вызова конструктора используется фабричная функция, которая решает, какой именно объект создать.

Проблема: жёсткая привязка к типам

Без фабрики код привязан к конкретным типам:

type MySQLDatabase struct{}
type PostgresDatabase struct{}

func main() {
    db := &MySQLDatabase{} // Жёсткая привязка
    // Чтобы сменить БД, нужно менять код
}

Каждый раз при смене типа приходится переписывать код.

Решение: фабричная функция

type Database interface {
    Connect() error
    Query(sql string) ([]Row, error)
}

func NewDatabase(dbType string) Database {
    switch dbType {
    case "mysql":
        return &MySQLDatabase{}
    case "postgres":
        return &PostgresDatabase{}
    default:
        return &MySQLDatabase{}
    }
}

Теперь тип определяется в одном месте. Клиентский код не знает о конкретных реализациях.

Simple Factory: базовая фабрика

Самый простой вариант - функция, которая возвращает интерфейс:

type Logger interface {
    Log(message string)
}

type FileLogger struct {
    path string
}

func (f *FileLogger) Log(message string) {
    // Запись в файл
}

type ConsoleLogger struct{}

func (c *ConsoleLogger) Log(message string) {
    fmt.Println(message)
}

func NewLogger(logType string) Logger {
    if logType == "file" {
        return &FileLogger{path: "/var/log/app.log"}
    }
    return &ConsoleLogger{}
}

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

logger := NewLogger("console")
logger.Log("Application started")

Factory Method: фабрика в интерфейсе

Когда нужно делегировать создание объектов подклассам:

type Notification interface {
    Send(message string) error
}

type NotificationFactory interface {
    CreateNotification() Notification
}

type EmailFactory struct{}

func (e *EmailFactory) CreateNotification() Notification {
    return &EmailNotification{}
}

type SMSFactory struct{}

func (s *SMSFactory) CreateNotification() Notification {
    return &SMSNotification{}
}

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

func SendAlert(factory NotificationFactory, msg string) {
    notification := factory.CreateNotification()
    notification.Send(msg)
}

Abstract Factory: семейства объектов

Когда нужно создавать связанные объекты:

type UIFactory interface {
    CreateButton() Button
    CreateCheckbox() Checkbox
}

type WindowsFactory struct{}

func (w *WindowsFactory) CreateButton() Button {
    return &WindowsButton{}
}

func (w *WindowsFactory) CreateCheckbox() Checkbox {
    return &WindowsCheckbox{}
}

type MacFactory struct{}

func (m *MacFactory) CreateButton() Button {
    return &MacButton{}
}

func (m *MacFactory) CreateCheckbox() Checkbox {
    return &MacCheckbox{}
}

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

func RenderUI(factory UIFactory) {
    button := factory.CreateButton()
    checkbox := factory.CreateCheckbox()
    
    button.Render()
    checkbox.Render()
}

Фабрика с конфигурацией

type ServerConfig struct {
    Host string
    Port int
    TLS  bool
}

func NewServer(config ServerConfig) *Server {
    server := &Server{
        host: config.Host,
        port: config.Port,
    }
    
    if config.TLS {
        server.setupTLS()
    }
    
    return server
}

Фабрика с валидацией

func NewUser(email, password string) (*User, error) {
    if !isValidEmail(email) {
        return nil, errors.New("invalid email")
    }
    
    if len(password) < 8 {
        return nil, errors.New("password too short")
    }
    
    return &User{
        email:    email,
        password: hashPassword(password),
    }, nil
}

Регистрация фабрик

Для расширяемости без изменения кода:

type PaymentFactory func() Payment

var factories = make(map[string]PaymentFactory)

func RegisterPayment(name string, factory PaymentFactory) {
    factories[name] = factory
}

func CreatePayment(name string) Payment {
    factory, ok := factories[name]
    if !ok {
        return nil
    }
    return factory()
}

func init() {
    RegisterPayment("stripe", func() Payment {
        return &StripePayment{}
    })
    RegisterPayment("paypal", func() Payment {
        return &PayPalPayment{}
    })
}

Фабрика с пулом объектов

type ConnectionPool struct {
    connections chan *Connection
}

func NewConnectionPool(size int) *ConnectionPool {
    pool := &ConnectionPool{
        connections: make(chan *Connection, size),
    }
    
    for i := 0; i < size; i++ {
        pool.connections <- &Connection{}
    }
    
    return pool
}

func (p *ConnectionPool) Get() *Connection {
    return <-p.connections
}

func (p *ConnectionPool) Put(conn *Connection) {
    p.connections <- conn
}

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

1. Создание зависит от условий

func NewCache(size int) Cache {
    if size > 1000 {
        return &RedisCache{}
    }
    return &MemoryCache{}
}

2. Сложная инициализация

func NewHTTPClient(timeout time.Duration) *http.Client {
    return &http.Client{
        Timeout: timeout,
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
        },
    }
}

3. Скрытие реализации

func NewMetrics() Metrics {
    return &prometheusMetrics{
        registry: prometheus.NewRegistry(),
    }
}

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

1. Простые структуры

// Не нужно
func NewPoint(x, y int) *Point {
    return &Point{x: x, y: y}
}

// Достаточно
point := Point{X: 10, Y: 20}

2. Один тип

// Не нужно
func NewUser() *User {
    return &User{}
}

// Достаточно
user := &User{}

3. Нет логики создания

// Не нужно
func NewConfig() *Config {
    return &Config{}
}

// Достаточно
config := &Config{}

Фабрика vs Builder

// Factory: создание за один вызов
db := NewDatabase("postgres")

// Builder: пошаговое создание
db := NewDatabaseBuilder().
    WithHost("localhost").
    WithPort(5432).
    WithSSL(true).
    Build()

Factory для простых случаев. Builder для сложных объектов с множеством параметров.

Тестирование с фабриками

type UserRepository interface {
    Save(user *User) error
}

func NewUserRepository(env string) UserRepository {
    if env == "test" {
        return &MockRepository{}
    }
    return &PostgresRepository{}
}

func TestUserService(t *testing.T) {
    repo := NewUserRepository("test")
    service := NewUserService(repo)
    // Тест с mock-репозиторием
}

Производительность

func BenchmarkFactory(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = NewLogger("console")
    }
}

func BenchmarkDirect(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = &ConsoleLogger{}
    }
}

Фабрика добавляет минимальный overhead. Для большинства случаев разница незаметна.

Реальный пример: HTTP клиент

type HTTPClient interface {
    Get(url string) (*Response, error)
    Post(url string, body []byte) (*Response, error)
}

func NewHTTPClient(opts ...Option) HTTPClient {
    client := &httpClient{
        timeout: 30 * time.Second,
        retries: 3,
    }
    
    for _, opt := range opts {
        opt(client)
    }
    
    return client
}

type Option func(*httpClient)

func WithTimeout(d time.Duration) Option {
    return func(c *httpClient) {
        c.timeout = d
    }
}

func WithRetries(n int) Option {
    return func(c *httpClient) {
        c.retries = n
    }
}

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

client := NewHTTPClient(
    WithTimeout(10 * time.Second),
    WithRetries(5),
)

Заключение

Factory Pattern в Go:

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

Factory - это не про усложнение. Это про гибкость и расширяемость.

Если создание объекта - это одна строка, фабрика не нужна. Если создание зависит от условий, требует валидации или скрывает детали - фабрика поможет.

comments powered by Disqus