Files
cv-site/doc/_go-learning/patterns/08-factory-pattern.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

14 KiB

Factory Pattern in Go

Pattern Overview

The Factory Pattern provides an interface for creating objects without specifying the exact class of object that will be created. In Go, this is typically implemented through constructor functions that encapsulate complex object creation logic.

Pattern Structure

// Factory function
func NewObject(config Config) (*Object, error) {
    // Complex initialization logic
    obj := &Object{
        field1: config.Value1,
        field2: config.Value2,
    }

    // Validation
    if err := obj.validate(); err != nil {
        return nil, err
    }

    // Setup
    if err := obj.initialize(); err != nil {
        return nil, err
    }

    return obj, nil
}

Real Implementation: Error Factories

Domain Error Constructors

// internal/handlers/errors.go

// NewDomainError is the base error factory
func NewDomainError(code ErrorCode, message string, statusCode int) *DomainError {
    return &DomainError{
        Code:       code,
        Message:    message,
        StatusCode: statusCode,
    }
}

// Specific error factories
func InvalidLanguageError(lang string) *DomainError {
    return NewDomainError(
        ErrCodeInvalidLanguage,
        fmt.Sprintf("Unsupported language: %s (use 'en' or 'es')", lang),
        http.StatusBadRequest,
    ).WithField("lang")
}

func InvalidLengthError(length string) *DomainError {
    return NewDomainError(
        ErrCodeInvalidLength,
        fmt.Sprintf("Invalid CV length: %s (use 'short' or 'long')", length),
        http.StatusBadRequest,
    ).WithField("length")
}

func PDFGenerationError(err error) *DomainError {
    return NewDomainError(
        ErrCodePDFGeneration,
        "Failed to generate PDF. Please try again.",
        http.StatusInternalServerError,
    ).WithErr(err)
}

func DataNotFoundError(dataType, lang string) *DomainError {
    return NewDomainError(
        ErrCodeDataNotFound,
        fmt.Sprintf("%s data not found for language: %s", dataType, lang),
        http.StatusInternalServerError,
    )
}

Response Factories

// internal/handlers/types.go

// NewAPIResponse creates a success response
func NewAPIResponse(data interface{}) *APIResponse {
    return &APIResponse{
        Success: true,
        Data:    data,
        Meta: &MetaInfo{
            Timestamp: time.Now(),
        },
    }
}

// NewErrorResponse creates an error response
func NewErrorResponse(code, message string) *APIResponse {
    return &APIResponse{
        Success: false,
        Error: &ErrorInfo{
            Code:    code,
            Message: message,
        },
        Meta: &MetaInfo{
            Timestamp: time.Now(),
        },
    }
}

// NewPDFExportRequest creates a validated PDF export request
func NewPDFExportRequest() *PDFExportRequest {
    return &PDFExportRequest{
        Lang:    "en",
        Length:  "short",
        Icons:   "show",
        Version: "with_skills",
    }
}

Handler Factories

CVHandler Factory

// internal/handlers/cv.go

// NewCVHandler creates a new CV handler with all dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
    return &CVHandler{
        tmpl: tmpl,
        host: host,
    }
}

// With validation
func NewCVHandlerWithValidation(
    tmpl *templates.Manager,
    host string,
) (*CVHandler, error) {
    if tmpl == nil {
        return nil, errors.New("template manager is required")
    }
    if host == "" {
        return nil, errors.New("host is required")
    }

    return &CVHandler{
        tmpl: tmpl,
        host: host,
    }, nil
}

Template Manager Factory

// internal/templates/manager.go

// NewManager creates and initializes a template manager
func NewManager(config *config.TemplateConfig) (*Manager, error) {
    // Validate config
    if config == nil {
        return nil, errors.New("config is required")
    }
    if config.Dir == "" {
        return nil, errors.New("template directory is required")
    }

    // Create manager
    m := &Manager{
        templates: make(map[string]*template.Template),
        config:    config,
    }

    // Load templates
    if err := m.loadTemplates(); err != nil {
        return nil, fmt.Errorf("load templates: %w", err)
    }

    log.Printf("Template manager initialized with %d templates", len(m.templates))
    return m, nil
}

Factory with Options Pattern

Functional Options

// Option function type
type HandlerOption func(*Handler)

// Option constructors
func WithTimeout(d time.Duration) HandlerOption {
    return func(h *Handler) {
        h.timeout = d
    }
}

func WithMaxRetries(n int) HandlerOption {
    return func(h *Handler) {
        h.maxRetries = n
    }
}

func WithLogger(logger Logger) HandlerOption {
    return func(h *Handler) {
        h.logger = logger
    }
}

// Factory with options
func NewHandler(tmpl *templates.Manager, opts ...HandlerOption) *Handler {
    h := &Handler{
        tmpl:       tmpl,
        timeout:    30 * time.Second,  // Defaults
        maxRetries: 3,
        logger:     &DefaultLogger{},
    }

    // Apply options
    for _, opt := range opts {
        opt(h)
    }

    return h
}

// Usage
handler := NewHandler(
    tmplManager,
    WithTimeout(10*time.Second),
    WithMaxRetries(5),
    WithLogger(customLogger),
)

Options Struct

// Options struct approach
type HandlerOptions struct {
    Timeout    time.Duration
    MaxRetries int
    Logger     Logger
}

// DefaultOptions provides sensible defaults
func DefaultOptions() *HandlerOptions {
    return &HandlerOptions{
        Timeout:    30 * time.Second,
        MaxRetries: 3,
        Logger:     &DefaultLogger{},
    }
}

// Factory with options
func NewHandler(tmpl *templates.Manager, opts *HandlerOptions) *Handler {
    if opts == nil {
        opts = DefaultOptions()
    }

    return &Handler{
        tmpl:       tmpl,
        timeout:    opts.Timeout,
        maxRetries: opts.MaxRetries,
        logger:     opts.Logger,
    }
}

// Usage
handler := NewHandler(tmplManager, &HandlerOptions{
    Timeout:    10 * time.Second,
    MaxRetries: 5,
})

Abstract Factory Pattern

Database Factory

// Database interface
type Database interface {
    Query(query string) (Result, error)
    Close() error
}

// Concrete implementations
type PostgresDB struct {
    conn *sql.DB
}

type MySQLDB struct {
    conn *sql.DB
}

type SQLiteDB struct {
    conn *sql.DB
}

// Factory function
func NewDatabase(dbType, connString string) (Database, error) {
    switch dbType {
    case "postgres":
        conn, err := sql.Open("postgres", connString)
        if err != nil {
            return nil, err
        }
        return &PostgresDB{conn: conn}, nil

    case "mysql":
        conn, err := sql.Open("mysql", connString)
        if err != nil {
            return nil, err
        }
        return &MySQLDB{conn: conn}, nil

    case "sqlite":
        conn, err := sql.Open("sqlite3", connString)
        if err != nil {
            return nil, err
        }
        return &SQLiteDB{conn: conn}, nil

    default:
        return nil, fmt.Errorf("unsupported database type: %s", dbType)
    }
}

// Usage
db, err := NewDatabase("postgres", "connection-string")

Factory with Builder Pattern

Request Builder

// Builder pattern for complex object construction
type RequestBuilder struct {
    req *http.Request
    err error
}

// NewRequestBuilder creates a new request builder
func NewRequestBuilder(method, url string) *RequestBuilder {
    req, err := http.NewRequest(method, url, nil)
    return &RequestBuilder{
        req: req,
        err: err,
    }
}

// Builder methods
func (b *RequestBuilder) WithHeader(key, value string) *RequestBuilder {
    if b.err != nil {
        return b
    }
    b.req.Header.Set(key, value)
    return b
}

func (b *RequestBuilder) WithBody(body io.Reader) *RequestBuilder {
    if b.err != nil {
        return b
    }
    b.req.Body = io.NopCloser(body)
    return b
}

func (b *RequestBuilder) WithContext(ctx context.Context) *RequestBuilder {
    if b.err != nil {
        return b
    }
    b.req = b.req.WithContext(ctx)
    return b
}

// Build finalizes and returns the request
func (b *RequestBuilder) Build() (*http.Request, error) {
    return b.req, b.err
}

// Usage
req, err := NewRequestBuilder("POST", "https://api.example.com").
    WithHeader("Content-Type", "application/json").
    WithBody(bytes.NewBuffer(data)).
    WithContext(ctx).
    Build()

Factory Method Pattern

Data Loader Factory

// Loader interface
type DataLoader interface {
    Load(lang string) (interface{}, error)
}

// Concrete loaders
type CVLoader struct{}

func (l *CVLoader) Load(lang string) (interface{}, error) {
    return cvmodel.LoadCV(lang)
}

type UILoader struct{}

func (l *UILoader) Load(lang string) (interface{}, error) {
    return uimodel.LoadUI(lang)
}

// Factory method
func NewLoader(loaderType string) (DataLoader, error) {
    switch loaderType {
    case "cv":
        return &CVLoader{}, nil
    case "ui":
        return &UILoader{}, nil
    default:
        return nil, fmt.Errorf("unknown loader type: %s", loaderType)
    }
}

// Usage
loader, err := NewLoader("cv")
if err != nil {
    return err
}
data, err := loader.Load("en")

Factory Registry Pattern

Handler Registry

// Handler factory registry
type HandlerFactory func() http.Handler

var handlerRegistry = make(map[string]HandlerFactory)

// Register handler factory
func RegisterHandler(name string, factory HandlerFactory) {
    handlerRegistry[name] = factory
}

// Get handler from registry
func GetHandler(name string) (http.Handler, error) {
    factory, ok := handlerRegistry[name]
    if !ok {
        return nil, fmt.Errorf("handler not found: %s", name)
    }
    return factory(), nil
}

// Register handlers at init
func init() {
    RegisterHandler("home", func() http.Handler {
        return http.HandlerFunc(handleHome)
    })

    RegisterHandler("about", func() http.Handler {
        return http.HandlerFunc(handleAbout)
    })
}

// Usage
handler, err := GetHandler("home")

Real-World Factory Examples

1. HTTP Client Factory

// NewHTTPClient creates configured HTTP client
func NewHTTPClient(timeout time.Duration, maxRetries int) *http.Client {
    return &http.Client{
        Timeout: timeout,
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
        },
    }
}

// With retry logic
func NewRetryableHTTPClient(timeout time.Duration, maxRetries int) *http.Client {
    client := NewHTTPClient(timeout, maxRetries)
    // Wrap with retry logic
    return client
}

2. Logger Factory

// Logger factory with different outputs
func NewLogger(output string) (*log.Logger, error) {
    switch output {
    case "stdout":
        return log.New(os.Stdout, "[APP] ", log.LstdFlags), nil

    case "stderr":
        return log.New(os.Stderr, "[APP] ", log.LstdFlags), nil

    case "file":
        f, err := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
        if err != nil {
            return nil, err
        }
        return log.New(f, "[APP] ", log.LstdFlags), nil

    default:
        return nil, fmt.Errorf("unknown output: %s", output)
    }
}

3. Middleware Factory

// Middleware factory
func NewAuthMiddleware(tokenValidator TokenValidator) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")

            if err := tokenValidator.Validate(token); err != nil {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// Usage
authMiddleware := NewAuthMiddleware(&JWTValidator{})
handler := authMiddleware(myHandler)

Benefits

  1. Encapsulation: Complex creation logic is hidden
  2. Consistency: All objects created the same way
  3. Flexibility: Easy to change implementation
  4. Testability: Easy to create test objects
  5. Validation: Centralized validation in factory

Best Practices

DO

// Validate inputs in factory
func NewHandler(config *Config) (*Handler, error) {
    if config == nil {
        return nil, errors.New("config is required")
    }
    return &Handler{config: config}, nil
}

// Return errors for creation failures
func NewDatabase(connString string) (*Database, error) {
    db, err := sql.Open("postgres", connString)
    if err != nil {
        return nil, fmt.Errorf("open database: %w", err)
    }
    return &Database{db: db}, nil
}

// Provide sensible defaults
func NewHandler(opts *Options) *Handler {
    if opts == nil {
        opts = DefaultOptions()
    }
    return &Handler{opts: opts}
}

// Use descriptive factory names
func NewRetryableHTTPClient(...) *http.Client
func NewCachedDatabase(...) *Database
func NewBufferedWriter(...) *Writer

DON'T

// DON'T return panics from factories
func NewHandler() *Handler {
    config := loadConfig()
    if config == nil {
        panic("no config")  // Wrong! Return error
    }
    return &Handler{config: config}
}

// DON'T ignore errors
func NewHandler() *Handler {
    db, _ := connectDB()  // Wrong! Handle error
    return &Handler{db: db}
}

// DON'T make factories too complex
func NewHandler(...20 parameters...) *Handler {
    // Too many parameters! Use options pattern
}

Testing Factories

func TestNewHandler(t *testing.T) {
    t.Run("Valid config", func(t *testing.T) {
        config := &Config{Timeout: 10}
        handler, err := NewHandler(config)

        if err != nil {
            t.Errorf("unexpected error: %v", err)
        }
        if handler == nil {
            t.Error("expected handler, got nil")
        }
    })

    t.Run("Nil config", func(t *testing.T) {
        handler, err := NewHandler(nil)

        if err == nil {
            t.Error("expected error for nil config")
        }
        if handler != nil {
            t.Error("expected nil handler")
        }
    })
}
  • Builder Pattern: For complex, multi-step object creation
  • Singleton Pattern: Factories can create singletons
  • Dependency Injection: Factories inject dependencies

Further Reading