ការបង្កើត កម្រិតស្មុគស្មាញ: មធ្យម

លំនាំ Factory Method ក្នុង Go

ប្រើ constructor functions និង interfaces តូចៗ ដើម្បីជ្រើស concrete implementations ដោយមិនភ្ជាប់ callers ទៅនឹងវា។

The Problem

In Go codebases, callers often need a value that satisfies an interface, but they should not care which concrete type gets instantiated. If that construction logic is scattered across handlers and services, every new case adds more switch statements and more coupling to concrete structs.

The Solution

In Go, Factory Method usually looks like a small interface plus constructor functions. Each factory decides which concrete type to build, validates the inputs, and returns the abstract Product. Callers depend on behavior, not struct names.

Structure

  • Product interface: The common behavior callers need.
  • Concrete products: Physical, digital, and subscription product types.
  • Factory interface: Declares the factory method for building a Product.
  • Concrete factories: Each one encapsulates construction details for a product family.
  • Client: A service or handler that accepts a factory dependency.

Implementation

This example uses three factories to create different commerce products. Each factory keeps creation details close to the type it knows how to build, while the client only works with the shared interface.

package main

import "fmt"

// Product is the abstraction returned by each factory.
type Product interface {
	Name() string
	Price() float64
	Fulfill() string
}

type PhysicalProduct struct {
	name  string
	price float64
	sku   string
}

func NewPhysicalProduct(name string, price float64, sku string) *PhysicalProduct {
	return &PhysicalProduct{name: name, price: price, sku: sku}
}

func (p *PhysicalProduct) Name() string   { return p.name }
func (p *PhysicalProduct) Price() float64 { return p.price }
func (p *PhysicalProduct) Fulfill() string {
	return fmt.Sprintf("ship physical item with SKU %s", p.sku)
}

type DigitalProduct struct {
	name     string
	price    float64
	fileSize int64
}

func NewDigitalProduct(name string, price float64, fileSize int64) *DigitalProduct {
	return &DigitalProduct{name: name, price: price, fileSize: fileSize}
}

func (p *DigitalProduct) Name() string   { return p.name }
func (p *DigitalProduct) Price() float64 { return p.price }
func (p *DigitalProduct) Fulfill() string {
	return fmt.Sprintf("deliver download link for %d MB file", p.fileSize)
}

type SubscriptionProduct struct {
	name         string
	price        float64
	billingCycle string
}

func NewSubscriptionProduct(name string, price float64, billingCycle string) *SubscriptionProduct {
	return &SubscriptionProduct{name: name, price: price, billingCycle: billingCycle}
}

func (p *SubscriptionProduct) Name() string   { return p.name }
func (p *SubscriptionProduct) Price() float64 { return p.price }
func (p *SubscriptionProduct) Fulfill() string {
	return fmt.Sprintf("activate %s subscription", p.billingCycle)
}

Best Practices

  • Keep interfaces small. The factory should return the narrow behavior the caller needs, not a wide god interface.
  • Put validation inside the factory. That keeps construction rules from leaking into every caller.
  • Prefer constructor functions over direct struct literals when invariants matter.
  • Use functional options when object setup becomes broad or optional.
  • Do not add a factory if one constructor call is already clear. The pattern earns its keep only when it removes branching and coupling.

When to Use

  • Multiple concrete implementations satisfy the same interface.
  • Construction rules vary by type and you want to keep them centralized.
  • Callers should not know about concrete struct selection.

When NOT to Use

  • There is only one implementation and no real branching.
  • A plain NewType(...) constructor is already clear enough.
  • The abstraction would just add ceremony without removing coupling.