Iniezione delle dipendenze
Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 45 di 107.
L'iniezione delle dipendenze è una tecnica in cui una struct riceve le proprie dipendenze dall'esterno anziché crearle al suo interno. In Go, le interfacce rendono questo schema naturale ed efficace.
Invece di codificare rigidamente un’implementazione specifica all’interno di una struct, accetti un’interfaccia. Questo ti permette di sostituire le implementazioni senza modificare il codice della struct:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Ora crea una struct che dipende dall’interfaccia, non da un tipo concreto:
type OrderService struct {
notifier Notifier // dipendenza iniettata tramite interfaccia
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
OrderService non sa né gli interessa se sta usando email o SMS. Inietti la dipendenza quando crei il servizio:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
}
Questo approccio rende il tuo codice più flessibile e testabile. Durante i test, puoi inserire un notificatore simulato che non invia davvero messaggi. In produzione, inserisci l’implementazione reale. La struct rimane invariata in entrambi gli scenari.
Sfida
FacileCostruiamo un sistema di logging che dimostra la potenza dell'iniezione delle dipendenze. Creerai un servizio che può scrivere log in destinazioni diverse, senza che il servizio sappia o si preoccupi di dove finiscano effettivamente quei log.
Organizzerai il codice in tre file:
logger.go: Definisci un'interfacciaLoggerche richiede un metodoLog(message string) string. Crea poi due implementazioni diverse di logger:ConsoleLoggercon un campoPrefix. Il suo metodoLogrestituisce[CONSOLE] [Prefix]: [message]FileLoggercon un campoFilename. Il suo metodoLogrestituisce[FILE:[Filename]] [message]
service.go: Crea una structAppServiceche dipende dall'interfacciaLogger(non da un tipo concreto). Includi una funzione costruttriceNewAppServiceche accetta unLoggere restituisce un puntatore aAppService. Aggiungi aAppServiceun metodo chiamatoDoWork(task string) stringche usa il logger iniettato per registrare il messaggioProcessing: [task]e restituisce ciò che restituisce il logger.main.go: Leggi la configurazione dall'input, crea entrambi i tipi di logger, inietta ciascuno in un'istanza separata diAppServicee chiamaDoWorksu ciascun servizio con l'attività fornita. Stampa il risultato di ogni chiamata al servizio.
Verranno forniti i seguenti input:
- Riga 1: Prefisso del logger per la console
- Riga 2: Nome file del logger su file
- Riga 3: Attività da elaborare
Ad esempio, dati INFO, app.log e user authentication, l'output dovrebbe essere:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationNota che AppService non sa se sta registrando i log sulla console o su un file. Si limita a chiamare Log sul logger che gli è stato iniettato. Questa flessibilità è l'essenza dell'iniezione delle dipendenze: lo stesso codice del servizio funziona con implementazioni di logging completamente diverse.
Provalo tu
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leggi il prefisso del logger della console
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Leggi il nome del file del logger su file
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Leggi l'attività da elaborare
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Crea un ConsoleLogger con prefix
// TODO: Crea un FileLogger con filename
// TODO: Crea un AppService con ConsoleLogger iniettato
// TODO: Crea un altro AppService con FileLogger iniettato
// TODO: Chiama DoWork su ogni servizio con task e stampa i risultati
fmt.Println("result1")
fmt.Println("result2")
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti di OOP in Go
File esterniWorkspace e moduli GoPacchetti e importazioniNomi esportati e non esportatiIntroduzione all’OOP in GoGli struct come classiDefinire metodi sugli structReceiver con puntatore vs receiver con valoreInizializzazione degli structFunzioni costruttriciRiepilogo - Calcolatrice semplice4Interfacce
Introduzione alle interfacceImplementazione implicitaL’interfaccia come contrattoInterfaccia vuota (any)Asserzione di tipoSwitch sul tipoComposizione di interfacceInterfacce Stringer ed ErrorRipasso - Calcolatore di forme7Incapsulamento
Campi esportati e non esportatiIncapsulamento a livello di packageMetodi Getter e SetterNascondimento delle informazioni in GoRiepilogo - Registri degli studenti10Tipi generici (Go 1.18+)
Introduzione ai tipi genericiParametri di tipoVincoli di tipoStruct genericheSoluzione alternativa per i metodi genericiRiepilogo - Collezione generica13Modelli di progettazione Parte 1
Introduzione ai modelli di progettazioneModello SingletonModello FactoryModello Abstract FactoryModello ObserverModello Strategy2Approfondimento su tipi e struct
Tipi di base e compostiDefinizioni di tipi personalizzatiTag delle structStruct anonimeStruct annidateValori zero e predefinitiRipasso - Rubrica5Composizione invece dell'ereditarietà
Perché Go non ha l'ereditarietàFondamenti dell'embedding delle structPromozione dei metodiEmbedding di più structEmbedding e aggregazione a confrontoMascheramento dei metodi embeddedRiepilogo: gerarchia dei dipendenti8Gestione degli errori e OOP
L'interfaccia errorTipi di errore personalizzatiWrapping degli errori (fmt.Errorf)Errori sentinellaerrors.Is() ed errors.As()Panic, Defer e RecoverRiepilogo - Parser di file11Libreria standard e OOP
io.Reader e io.Writersort.InterfaceInterfaccia fmt.Stringerencoding/json con le structInterfaccia http.HandlerRipasso - modelli REST API14Modelli di progettazione – Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeMiddleware come Decorator3Puntatori e memoria
Concetti base dei puntatori in GoPuntatori a structPassaggio per valore vs per riferimentoLa funzione new()Garbage collection in GoRiepilogo - Costruttore di liste concatenate6Polimorfismo in Go
Polimorfismo tramite interfacceDuck typing in GoRegole di soddisfacimento delle interfacceCollezioni polimorficheIniezione delle dipendenzeRiepilogo - Elaboratore di pagamenti9Concorrenza e OOP
Basi delle goroutineCanali e comunicazioneCanali bufferizzati vs non bufferizzatiIstruzione selectsync.Mutex e sync.RWMutexsync.WaitGroupProgettazione di struct thread-safeRiepilogo: worker poolEsercitati da solo: Compilatore Go online