Warum Go keine Vererbung hat
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 34 von 107.
Traditionelle objektorientierte Sprachen wie Java und C++ verwenden Vererbung, um Code zwischen Typen gemeinsam zu nutzen. Eine Kindklasse erweitert eine Elternklasse und erbt alle ihre Felder und Methoden. Go lässt diese Funktionalität bewusst weg.
Vererbung erzeugt eine enge Kopplung zwischen Typen. Wenn sich eine Elternklasse ändert, sind alle Unterklassen betroffen.
Tiefe Vererbungshierarchien werden schwer verständlich und wartbar. Das „Problem der fragilen Basisklasse“ tritt auf, wenn Änderungen an einer Basisklasse unerwartet abgeleitete Klassen beschädigen.
Die Designer von Go haben einen anderen Weg gewählt: Komposition statt Vererbung. Anstatt zu sagen: „Ein Dog ist ein Animal“, ermutigt Go dich zu sagen: „Ein Dog has Animal-ähnliche Verhaltensweisen.“ Diese subtile Verschiebung führt zu flexiblerem und wartungsfreundlicherem Code.
Go erreicht die Wiederverwendung von Code durch zwei Mechanismen, die du bereits kennengelernt hast:
- Interfaces definieren Verhaltensverträge ohne Implementierungsdetails
- Struct-Einbettung ermöglicht es Typen, andere Typen einzuschließen und deren Methoden wiederzuverwenden
Betrachte diesen Vergleich. In der traditionellen OOP würdest du möglicherweise class Dog extends Animal schreiben. In Go bettest du eine Animal-Struktur in Dog ein und implementierst gemeinsame Schnittstellen. Das Ergebnis ist eine ähnliche Funktionalität mit einer geringeren Kopplung zwischen den Typen.
Dieses Kapitel untersucht die Struktur-Einbettung ausführlich und zeigt, wie Go die Vorteile der Vererbung ohne deren Nachteile erzielt.
Aufgabe
EinfachErstellen wir ein Benachrichtigungssystem, das den Kompositionsansatz von Go anstelle von Vererbung demonstriert. Du erstellst Typen, die ihr Verhalten über Interfaces und Struct-Einbettung statt über Klassenhierarchien teilen.
Du organisierst deinen Code über drei Dateien:
notifier.go: Definiere einNotifier-Interface mit einer einzigen MethodeNotify(message string) string. Erstelle außerdem eineBaseNotifier-Struct mit einemName-Feld, die von anderen Typen eingebettet wird. GibBaseNotifiereine Methode namensFormat(message string) string, die die Nachricht mit dem Namen des Benachrichtigers in eckigen Klammern als Präfix zurückgibt.channels.go: Erstelle zwei Typen für Benachrichtigungskanäle, dieBaseNotifiereinbetten und dasNotifier-Interface implementieren:EmailNotifiermit einem zusätzlichenAddress-FeldSMSNotifiermit einem zusätzlichenPhone-Feld
Notify-Methode jedes Typs sollte die eingebetteteFormat-Methode verwenden und die spezifischen Kanalinformationen in die Ausgabe aufnehmen.main.go: Erstelle eine Funktion namensSendAlert, die ein beliebigesNotifierund eine Nachricht akzeptiert und anschließend das Ergebnis des Aufrufs vonNotifyzurückgibt. Lies die Benachrichtigungsdetails aus der Eingabe, erstelle beide Arten von Benachrichtigern und demonstriere, wie sie über das Interface austauschbar verwendet werden können.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Name des Benachrichtigers für E-Mail
- Zeile 2: E-Mail-Adresse
- Zeile 3: Name des Benachrichtigers für SMS
- Zeile 4: Telefonnummer
- Zeile 5: Benachrichtigungsnachricht
Deine Format-Methode auf BaseNotifier sollte Folgendes zurückgeben:
[Name] messageDeine Notify-Methoden sollten Folgendes zurückgeben:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Wenn beispielsweise Alerts, user@mail.com, Urgent, 555-1234 und Server down gegeben sind, sollte deine Ausgabe wie folgt aussehen:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downBeachte, wie beide Benachrichtigertypen die Format-Methode von BaseNotifier durch Einbettung wiederverwenden, während jeder seine eigene Notify-Implementierung bereitstellt. Die Funktion SendAlert funktioniert mit jedem Notifier, ohne den konkreten Typ zu kennen. Das ist Komposition statt Vererbung in der Praxis.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert akzeptiert einen beliebigen Notifier und eine Nachricht und gibt das Ergebnis des Aufrufs von Notify zurück
// TODO: Implementiere die SendAlert-Funktion
func main() {
scanner := bufio.NewScanner(os.Stdin)
// E-Mail-Notifier-Namen lesen
scanner.Scan()
emailName := scanner.Text()
// E-Mail-Adresse lesen
scanner.Scan()
emailAddress := scanner.Text()
// SMS-Notifier-Namen lesen
scanner.Scan()
smsName := scanner.Text()
// Telefonnummer lesen
scanner.Scan()
phoneNumber := scanner.Text()
// Alarmnachricht lesen
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Erstelle einen EmailNotifier mit emailName und emailAddress
// TODO: Erstelle einen SMSNotifier mit smsName und phoneNumber
// TODO: Verwende SendAlert, um die alertMessage über beide Notifier zu senden
// und gib die Ergebnisse aus
// Unterdrücke Warnungen zu ungenutzten Variablen (entferne diese, wenn du die Variablen verwendest)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler