Vai al contenuto
eLearner.app
Modulo 5 · Lezione 3 di 523/50 nel corso~12 min
Lezioni del modulo (3/5)

Composizione (embedding)

Go non ha ereditarietà. La risposta idiomatica al riuso è la composizione, e in particolare l'embedding: includere un tipo dentro un altro senza dargli un nome di campo. I campi e i metodi del tipo incorporato vengono promossi, diventando direttamente accessibili come se appartenessero al tipo esterno.

Embedding di struct

Go
type Animal struct {
    Name string
}

func (a Animal) Greet() string {
    return "ciao da " + a.Name
}

type Dog struct {
    Animal        // embedding — NESSUN nome di campo
    Breed string
}

c := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Pastore",
}

fmt.Println(c.Name)         // "Rex" — promosso da Animal
fmt.Println(c.Greet())      // "ciao da Rex" — metodo promosso
fmt.Println(c.Breed)        // campo proprio

Il campo embedded è comunque accessibile col nome del tipo: c.Animal.Name funziona ed è identico a c.Name. Serve quando ci sono collisioni.

Override (parziale): "shadowing" di metodi

Se il tipo esterno definisce un metodo con lo stesso nome di quello promosso, il suo metodo "vince". Da dentro puoi sempre raggiungere quello embedded col nome del tipo:

Go
func (c Dog) Greet() string {
    return "BAU! " + c.Animal.Greet()
}

Non è ereditarietà polimorfica: è solo risoluzione lessicale dei nomi. Per il polimorfismo si usano le interfacce (Modulo 6).

Embedding multiplo

Puoi incorporare più tipi:

Go
type Logger struct{ /*...*/ }
func (l Logger) Log(msg string) { /*...*/ }

type Timer struct{ /*...*/ }
func (t Timer) Now() time.Time { /*...*/ }

type Service struct {
    Logger
    Timer
    Name string
}

s := Service{Name: "api"}
s.Log("start")   // promosso da Logger
s.Now()          // promosso da Timer

Se due tipi incorporati hanno un metodo con LO STESSO nome, l'accesso diretto diventa ambiguo e il compilatore richiede la forma esplicita (s.Logger.Foo() o s.Timer.Foo()).

Embedding di interfacce

Anche le interfacce si possono "embeddare" (vedi Modulo 6):

Go
type ReadWriter interface {
    io.Reader
    io.Writer
}

Equivale all'unione dei metodi di Reader e Writer.

Quando usare la composizione esplicita (non embedding)

Se vuoi un campo nominato e non l'auto-promozione, usa la normale composizione:

Go
type Dog struct {
    Anim  Animal    // campo NORMALE, niente promozione
    Breed string
}
c.Anim.Name   // serve il nome del campo

L'embedding è uno strumento di delega comodo, non una scelta di design migliore della composizione esplicita: usalo quando vuoi davvero promuovere l'API del tipo interno.

Prova tu

Esercizio#go.m5.l3.e1
Tentativi: 0Caricamento…

Definisci Dog che incorpora Animal (embedding, senza nome di campo) e ha un campo Breed string.

Caricamento editor…
Mostra suggerimento

Embedding: scrivi il TIPO senza dargli un nome di campo.

Soluzione disponibile dopo 3 tentativi

Esercizio#go.m5.l3.e2
Tentativi: 0Caricamento…

Aggiungi ad Animal un metodo Greet() string e chiamalo direttamente su una variabile di tipo Dog (promozione del metodo).

Caricamento editor…
Mostra suggerimento

Il metodo definito su Animal viene promosso a Dog grazie all'embedding.

Soluzione disponibile dopo 3 tentativi

Quiz#go.m5.l3.e3
Pronto

Go supporta l'ereditarietà classica?

Go
type B struct { /* extends A? */ }
Opzioni di risposta

Recap

  • Niente ereditarietà: si compone con embedding.
  • Embedding = scrivere il TIPO senza nome di campo nella struct.
  • Campi e metodi del tipo incorporato vengono promossi.
  • Accesso esplicito sempre disponibile: outer.Inner.Campo.
  • Override "lessicale": il metodo del tipo esterno copre quello promosso.
  • Embedding ≠ is-a: per polimorfismo usa le interfacce.