d95c62bad4
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.
14 KiB
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
- Encapsulation: Complex creation logic is hidden
- Consistency: All objects created the same way
- Flexibility: Easy to change implementation
- Testability: Easy to create test objects
- 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")
}
})
}
Related Patterns
- Builder Pattern: For complex, multi-step object creation
- Singleton Pattern: Factories can create singletons
- Dependency Injection: Factories inject dependencies