Perché Go non ha l'ereditarietà
Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 34 di 107.
I linguaggi tradizionali orientati agli oggetti come Java e C++ usano l’ereditarietà per condividere il codice tra i tipi. Una classe figlia estende una classe genitore, ereditandone tutti i campi e i metodi. Go omette deliberatamente questa funzionalità.
L’ereditarietà crea un forte accoppiamento tra i tipi. Quando una classe genitore cambia, tutte le classi figlie ne risentono.
Le gerarchie di ereditarietà profonde diventano difficili da comprendere e mantenere. Il «problema della classe base fragile» si verifica quando le modifiche a una classe base compromettono inaspettatamente le classi derivate.
I progettisti di Go hanno scelto una strada diversa: composizione anziché ereditarietà. Invece di dire «un Dog è un Animal», Go ti incoraggia a dire «un Dog ha comportamenti simili a quelli di Animal». Questo sottile cambiamento porta a un codice più flessibile e manutenibile.
Go realizza il riutilizzo del codice attraverso due meccanismi che hai già imparato:
- Le interfacce definiscono contratti di comportamento senza dettagli di implementazione
- L’incorporamento di struct consente ai tipi di includere altri tipi e riutilizzarne i metodi
Considera questo confronto. Nella programmazione OOP tradizionale, potresti scrivere class Dog extends Animal. In Go, incorpori una struct Animal all'interno di Dog e implementi interfacce condivise. Il risultato è una funzionalità simile con un accoppiamento più debole tra i tipi.
Questo capitolo approfondisce l’incorporamento di struct, mostrando come Go ottenga i vantaggi dell’ereditarietà senza i suoi svantaggi.
Sfida
FacileCreiamo un sistema di notifiche che dimostri l'approccio di Go basato sulla composizione anziché sull'ereditarietà. Creerai tipi che condividono comportamenti tramite interfacce e incorporamento di struct, anziché gerarchie di classi.
Organizzerai il codice in tre file:
notifier.go: Definisci un'interfacciaNotifiercon un unico metodoNotify(message string) string. Crea anche una structBaseNotifiercon un campoNameche verrà incorporato da altri tipi. Aggiungi aBaseNotifierun metodo chiamatoFormat(message string) stringche restituisce il messaggio preceduto dal nome del notificatore tra parentesi quadre.channels.go: Crea due tipi di canale di notifica che incorporanoBaseNotifiere implementano l'interfacciaNotifier:EmailNotifiercon un campo aggiuntivoAddressSMSNotifiercon un campo aggiuntivoPhone
Notifydi ciascun tipo deve usare il metodo incorporatoFormate includere nell'output le informazioni specifiche del canale.main.go: Crea una funzione chiamataSendAlertche accetta qualsiasiNotifiere un messaggio, quindi restituisce il risultato della chiamata aNotify. Leggi i dettagli delle notifiche dall'input, crea entrambi i tipi di notificatori e mostra come possono essere usati in modo intercambiabile tramite l'interfaccia.
Verranno forniti i seguenti input:
- Riga 1: Nome del notificatore per l'email
- Riga 2: Indirizzo email
- Riga 3: Nome del notificatore per SMS
- Riga 4: Numero di telefono
- Riga 5: Messaggio di avviso
Il metodo Format su BaseNotifier deve restituire:
[Name] messageI metodi Notify devono restituire:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Ad esempio, con Alerts, user@mail.com, Urgent, 555-1234 e Server down, l'output dovrebbe essere:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downNota come entrambi i tipi di notificatori riutilizzano il metodo Format di BaseNotifier tramite l'incorporamento, mentre ciascuno fornisce la propria implementazione di Notify. La funzione SendAlert funziona con qualsiasi Notifier senza conoscere il tipo concreto. Questa è la composizione al posto dell'ereditarietà in azione.
Provalo tu
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert accetta un qualsiasi Notifier e un messaggio e restituisce il risultato della chiamata a Notify
// TODO: Implementa la funzione SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Leggi il nome del notificatore email
scanner.Scan()
emailName := scanner.Text()
// Leggi l'indirizzo email
scanner.Scan()
emailAddress := scanner.Text()
// Leggi il nome del notificatore SMS
scanner.Scan()
smsName := scanner.Text()
// Leggi il numero di telefono
scanner.Scan()
phoneNumber := scanner.Text()
// Leggi il messaggio di avviso
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Crea un EmailNotifier con emailName e emailAddress
// TODO: Crea un SMSNotifier con smsName e phoneNumber
// TODO: Usa SendAlert per inviare alertMessage tramite entrambi i notificatori
// e stampa i risultati
// Evita gli avvisi relativi alle variabili non utilizzate (rimuovi queste istruzioni quando usi le variabili)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
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