W języku Go wsparcie dla kompozycji realizowane jest poprzez mechanizm embedding — to możliwość włączenia jednej struktury do innej bez wyraźnej nazwy pola. Zakładano, że takie podejście pozwoli modelować dziedziczenie "po swojemu", jednocześnie zachowując prostotę języka i unikając wielu skomplikowań związanych z dziedziczeniem wielokrotnym.
Programista często pragnie rozszerzyć zachowanie lub interfejs pewnej podstawowej struktury, nie komplikując architektury. W Go często używa się embedding, co pozwala na bezpośredni dostęp do metod i pól zagnieżdżonego typu, jakby były zdefiniowane w strukturze nadrzędnej. Ale embedding ma swoje szczególności, a niewłaściwe zrozumienie mechanizmu może prowadzić do nieoczekiwanych błędów, takich jak konflikty nazw, podwójne embedding i niewłaściwe dziedziczenie metod.
Prawidłowe, oszczędne użycie embedding pozwala osiągnąć czystszą kompozycję. Należy pamiętać, że metody i pola są „podnoszone” tylko na jeden poziom i że embedding realizuje relację „has-a”, a nie „is-a”. Należy unikać konfliktów nazw i zdawać sobie sprawę, jak działa metoda odbieracza.
Przykład kodu:
package main import "fmt" type Engine struct { Power int } func (e Engine) Start() { fmt.Println("Silnik uruchomiony z mocą", e.Power) } type Car struct { Engine // embedding, a nie pole Engine jako engine Engine Brand string } func main() { c := Car{Engine: Engine{Power: 200}, Brand: "Toyota"} c.Start() // dostępny bezpośrednio fmt.Println(c.Power) // pole też zostało podniesione }
Kluczowe cechy:
Czy embedding może zrealizować dziedziczenie wielokrotne?
Nie, embedding nie realizuje dziedziczenia jak w OOP, to kompozycja. Można zagnieżdżać kilka innych struktur w jedną, ale w przypadku konfliktu metod występuje konflikt kompilacji, a nie złączenie.
Co się stanie, jeśli pola zagnieżdżonych struktur mają te same nazwy?
Wystąpi błąd kompilacji przy bezpośrednim odwołaniu: kompilator nie wie, do którego pola się odwołać. Konieczne jest jawne wskazanie ścieżki przez nazwę zagnieżdżonej struktury.
Przykład kodu:
type A struct {X int} type B struct {X int} type C struct { A B } func main() { c := C{} // c.X = 1 // błąd: niejednoznaczny selektor c.A.X = 1 // tylko w ten sposób }
Czy metody ze wskaźnikiem i bez niego są podnoszone jednakowo przy embedding?
Nie. Metody z wskaźnikiem odbieraczem są podnoszone tylko wtedy, gdy struktura również używa wskaźnika (c := &Car{}). Jeśli struktura jest przez wartość, metody z odbieraczem wskaźnikowym nie „podniosą się”.
Projekt z głęboko zagnieżdżoną strukturą, gdzie embedding jest używany do imitacji wielopoziomowego dziedziczenia. Nowicjusz nie rozumie, z której struktury pochodzi pole lub metoda, cały projekt staje się bardziej skomplikowany i „magiczny”.
Zalety:
Wady:
Użycie embedding do realizacji interfejsu, na przykład interfejs Logger jest realizowany przez embedding typu z metodą Println, co jest wyraźnie udokumentowane i pokryte testami.
Zalety:
Wady: