La versione originale di questo articolo aveva due paragrafi e un esempio con Dog.Speak(). Questo copre la sintassi ma manca completamente il punto. Il pattern Factory in Go non riguarda la creazione di animali. Riguarda la scrittura di codice in cui il layer chiamante non ha bisogno di sapere o importarsi di quale implementazione concreta riceve.
Il Problema Senza una Factory#
Considera un UserService che istanzia direttamente uno storage PostgreSQL:
package user
type UserService struct {
db *PostgresDB // tipo concreto, non un'interfaccia
}
func NewUserService() *UserService {
db := &PostgresDB{
Host: "localhost",
Port: 5432,
Database: "users",
}
db.Connect()
return &UserService{db: db}
}Questo e problematico per tre motivi:
- Non puoi testare
UserServicesenza un’istanza Postgres in esecuzione - Non puoi scambiare Postgres con SQLite in sviluppo senza toccare
UserService - Il servizio possiede il ciclo di vita della connessione al database – una responsabilita che non dovrebbe avere
Il Pattern Factory: Costruzione Basata su Interfacce#
Definisci un’interfaccia che esprime cosa puo fare lo storage, poi scrivi una factory che restituisce l’implementazione giusta in base alla configurazione:
package storage
import "context"
// Storage e l'interfaccia da cui dipende il resto del codebase.
// Non menziona mai Postgres, SQLite o qualsiasi backend concreto.
type Storage interface {
GetUser(ctx context.Context, id int64) (*User, error)
CreateUser(ctx context.Context, u *User) error
DeleteUser(ctx context.Context, id int64) error
}
type Config struct {
Type string // "postgres", "sqlite", "memory"
DSN string // connection string per postgres/sqlite
}
// NewStorage e la funzione factory.
// Il chiamante ottiene un'interfaccia Storage e non vede mai il tipo concreto.
func NewStorage(cfg Config) (Storage, error) {
switch cfg.Type {
case "postgres":
return newPostgresStorage(cfg.DSN)
case "sqlite":
return newSQLiteStorage(cfg.DSN)
case "memory":
return newMemoryStorage(), nil
default:
return nil, fmt.Errorf("tipo di storage sconosciuto: %q", cfg.Type)
}
}Usarla per il Testing#
L’implementazione in-memory non e un giocattolo. E la chiave che rende i test unitari veloci e autonomi:
package storage
import (
"context"
"fmt"
"sync"
)
type memoryStorage struct {
mu sync.RWMutex
users map[int64]*User
seq int64
}
func newMemoryStorage() *memoryStorage {
return &memoryStorage{users: make(map[int64]*User)}
}
func (m *memoryStorage) GetUser(ctx context.Context, id int64) (*User, error) {
m.mu.RLock()
defer m.mu.RUnlock()
u, ok := m.users[id]
if !ok {
return nil, fmt.Errorf("utente %d non trovato", id)
}
return u, nil
}
func (m *memoryStorage) CreateUser(ctx context.Context, u *User) error {
m.mu.Lock()
defer m.mu.Unlock()
m.seq++
u.ID = m.seq
m.users[m.seq] = u
return nil
}
func (m *memoryStorage) DeleteUser(ctx context.Context, id int64) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.users, id)
return nil
}Ora i test unitari non toccano mai un database:
package user_test
import (
"context"
"testing"
"github.com/example/app/storage"
"github.com/example/app/user"
)
func TestCreateUser(t *testing.T) {
// Storage in-memory: nessun database richiesto, nessuna pulizia necessaria
store, err := storage.NewStorage(storage.Config{Type: "memory"})
if err != nil {
t.Fatal(err)
}
svc := user.NewUserService(store)
created, err := svc.CreateUser(context.Background(), "alice", "alice@example.com")
if err != nil {
t.Fatalf("errore inaspettato: %v", err)
}
if created.ID == 0 {
t.Error("atteso ID non zero")
}
}Factory con Dependency Injection#
Il servizio stesso accetta l’interfaccia nel suo costruttore – non chiama la factory:
package user
import (
"context"
"fmt"
"github.com/example/app/storage"
)
type UserService struct {
store storage.Storage // interfaccia, non tipo concreto
}
// NewUserService accetta qualsiasi implementazione di Storage.
// La factory decide quale usare; il servizio non se ne preoccupa.
func NewUserService(store storage.Storage) *UserService {
return &UserService{store: store}
}
func (s *UserService) CreateUser(ctx context.Context, name, email string) (*storage.User, error) {
u := &storage.User{Name: name, Email: email}
if err := s.store.CreateUser(ctx, u); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return u, nil
}Nel punto di ingresso dell’applicazione, colleghi tutto:
func main() {
storageType := os.Getenv("STORAGE_TYPE")
if storageType == "" {
storageType = "postgres"
}
store, err := storage.NewStorage(storage.Config{
Type: storageType,
DSN: os.Getenv("DATABASE_URL"),
})
if err != nil {
log.Fatal(err)
}
userSvc := user.NewUserService(store)
// ... configura gli handler HTTP, ecc.
}Imposta STORAGE_TYPE=memory in CI. Imposta STORAGE_TYPE=postgres in produzione. Il codice del servizio e identico in entrambi gli ambienti.
Notification Sender: Una Factory nel Mondo Reale#
Lo stesso pattern si applica a qualsiasi backend che varia per ambiente:
package notify
import "context"
type Sender interface {
Send(ctx context.Context, to, subject, body string) error
}
type Config struct {
Type string // "smtp", "log", "noop"
SMTPHost string
SMTPPort int
From string
}
func NewSender(cfg Config) (Sender, error) {
switch cfg.Type {
case "smtp":
return newSMTPSender(cfg)
case "log":
// Scrive su stdout -- utile in sviluppo e CI
return &logSender{}, nil
case "noop":
// Scarta silenziosamente tutti i messaggi -- utile nei test
return &noopSender{}, nil
default:
return nil, fmt.Errorf("tipo di sender sconosciuto: %q", cfg.Type)
}
}La produzione usa smtp. I test di integrazione usano log per poter vedere cosa verrebbe inviato. I test unitari usano noop per saltare completamente le email. Nessuna ginnastica con le variabili d’ambiente, nessun framework di mocking.
Il Tool wire per Codebase Grandi#
Quando il grafo delle dipendenze cresce oltre una manciata di servizi, Google Wire genera il codice di wiring a tempo di compilazione. Definisci le funzioni provider (le tue factory) e Wire genera un wire_gen.go che costruisce il grafo completo degli oggetti senza riflessione a runtime.
Per la maggior parte delle applicazioni, il wiring manuale in main.go e piu pulito e leggibile. Ricorri a Wire quando il codice di wiring stesso diventa un onere di manutenzione.
Errori comuni e come evitarli
Restituire tipi concreti invece di interfacce
Se la tua factory restituisce *PostgresStorage invece di Storage, ogni chiamante e accoppiato a Postgres. L’intero scopo della factory e restituire l’interfaccia. Il chiamante non dovrebbe mai importare il pacchetto concreto.
Factory che fa troppo lavoro
Una funzione factory dovrebbe costruire e configurare. Non dovrebbe fare chiamate di rete, eseguire migration o fare il seeding dei dati. Mantieni la costruzione veloce e sposta il lavoro di inizializzazione in metodi espliciti Init() o Start() che i chiamanti controllano.
Nessuna implementazione “noop” o in-memory per il testing Se le uniche implementazioni sono “backend reali” (Postgres, SMTP, S3), i tuoi test devono fare mock a livello HTTP o avviare container per ogni esecuzione. Scrivi sempre un’implementazione minima in-memory o noop. Costa 30 righe e ripaga in velocita di test.
Restituire nil nel caso default
Restituire nil, nil nel caso default dello switch in una factory significa che i chiamanti ottengono un’interfaccia nil e un errore nil. Il call site andra in panic quando prova a usare il valore restituito. Restituisci sempre un errore esplicito quando il tipo richiesto e sconosciuto.
Se vuoi approfondire questi argomenti, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.