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 zu teilen. Eine Kindklasse erweitert eine Elternklasse und erbt alle ihre Felder und Methoden. Go verzichtet bewusst auf dieses Feature.
Vererbung erzeugt eine enge Kopplung zwischen Typen. Wenn sich eine Elternklasse ändert, sind alle Kindklassen betroffen.
Tiefe Vererbungshierarchien werden schwer zu verstehen und zu warten. Das „Fragile Base Class Problem“ tritt auf, wenn Änderungen an einer Basisklasse unerwartet abgeleitete Klassen beeinträchtigen.
Die Designer von Go haben einen anderen Weg gewählt: Komposition statt Vererbung. Anstatt zu sagen „ein Hund ist ein Tier“, ermutigt Go Sie dazu, zu sagen „ein Hund hat tierähnliche Verhaltensweisen“. Diese subtile Verschiebung führt zu flexiblerem, wartbarerem Code.
Go realisiert die Wiederverwendung von Code durch zwei Mechanismen, die Sie bereits kennengelernt haben:
- Interfaces definieren Verhaltensverträge ohne Implementierungsdetails
- Struct-Embedding ermöglicht es Typen, andere Typen einzuschließen und deren Methoden wiederzuverwenden
Betrachten Sie diesen Vergleich. In traditioneller OOP würden Sie vielleicht class Dog extends Animal schreiben. In Go betten Sie ein Animal-Struct in Dog ein und implementieren gemeinsame Schnittstellen. Das Ergebnis ist eine ähnliche Funktionalität mit einer loseren Kopplung zwischen den Typen.
Dieses Kapitel befasst sich ausführlich mit dem Einbetten von Structs und zeigt, wie Go die Vorteile der Vererbung ohne deren Nachteile nutzt.
Aufgabe
EinfachLassen Sie uns ein Benachrichtigungssystem bauen, das den Kompositionsansatz von Go anstelle von Vererbung demonstriert. Sie werden Typen erstellen, die Verhalten durch Schnittstellen und Struct-Einbettung teilen, anstatt durch Klassenhierarchien.
Sie werden Ihren Code über drei Dateien organisieren:
notifier.go: Definieren Sie einNotifier-Interface mit einer einzigen MethodeNotify(message string) string. Erstellen Sie außerdem einBaseNotifier-Struct mit einemName-Feld, das von anderen Typen eingebettet wird. Geben SieBaseNotifiereine Methode namensFormat(message string) string, die die Nachricht mit dem Namen des Notifiers in Klammern vorangestellt zurückgibt.channels.go: Erstellen Sie zwei Benachrichtigungskanal-Typen, dieBaseNotifiereinbetten und dasNotifier-Interface implementieren:EmailNotifiermit einem zusätzlichenAddress-FeldSMSNotifiermit einem zusätzlichenPhone-Feld
Notify-Methode jedes Typs sollte die eingebetteteFormat-Methode verwenden und ihre spezifischen Kanalinformationen in die Ausgabe einbeziehen.main.go: Erstellen Sie eine Funktion namensSendAlert, die einen beliebigenNotifierund eine Nachricht akzeptiert und dann das Ergebnis des Aufrufs vonNotifyzurückgibt. Lesen Sie die Benachrichtigungsdetails aus der Eingabe ein, erstellen Sie beide Arten von Notifiern und demonstrieren Sie, wie sie austauschbar über das Interface verwendet werden können.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Notifier-Name für E-Mail
- Zeile 2: E-Mail-Adresse
- Zeile 3: Notifier-Name für SMS
- Zeile 4: Telefonnummer
- Zeile 5: Alarmmeldung
Ihre Format-Methode auf BaseNotifier sollte Folgendes zurückgeben:
[Name] messageIhre Notify-Methoden sollten Folgendes zurückgeben:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Zum Beispiel, gegeben Alerts, user@mail.com, Urgent, 555-1234 und Server down, sollte Ihre Ausgabe wie folgt aussehen:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downBeachten Sie, wie beide Notifier-Typen die Format-Methode von BaseNotifier durch Einbettung wiederverwenden, während jeder seine eigene Notify-Implementierung bereitstellt. Die SendAlert-Funktion arbeitet mit jedem Notifier, ohne den konkreten Typ zu kennen – das ist Komposition über Vererbung in Aktion.
Spickzettel
Go verwendet Komposition statt Vererbung, um Code zwischen Typen zu teilen, wodurch die enge Kopplung und die Probleme der fragilen Basisklasse der traditionellen objektorientierten Vererbung vermieden werden.
Anstelle von Klassenhierarchien erreicht Go die Wiederverwendung von Code durch:
- Interfaces - definieren Verhaltensverträge ohne Implementierungsdetails
- Struct-Einbettung - ermöglicht es Typen, andere Typen einzuschließen und deren Methoden wiederzuverwenden
Anstatt zu sagen „ein Hund ist ein Tier“ (Vererbung), fördert Go „ein Hund hat tierähnliche Verhaltensweisen“ (Komposition).
Beispiel für Struct-Einbettung
Definieren Sie ein Basis-Struct, das eingebettet wird:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}Betten Sie das Basis-Struct in andere Typen ein, um dessen Felder und Methoden wiederzuverwenden:
type EmailNotifier struct {
BaseNotifier // eingebettetes Struct
Address string
}
func (e EmailNotifier) Notify(message string) string {
// Eingebettete Format-Methode kann direkt aufgerufen werden
return "Email to " + e.Address + ": " + e.Format(message)
}Definieren Sie ein Interface, das mehrere Typen implementieren können:
type Notifier interface {
Notify(message string) string
}Verwenden Sie das Interface, um austauschbar mit verschiedenen konkreten Typen zu arbeiten:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}Dieser Ansatz bietet eine ähnliche Funktionalität wie die Vererbung, jedoch mit einer loseren Kopplung zwischen den Typen.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert akzeptiert jeden Notifier und eine Nachricht, gibt das Ergebnis des Notify-Aufrufs 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 die Ergebnisse auszugeben
// Warnungen wegen nicht verwendeter Variablen unterdrücken (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 Go OOP
Externe DateienGo Workspace & ModulePackages & ImportsExportierte vs. nicht-exportierte NamenEinführung in OOP in GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktor-FunktionenZusammenfassung – Einfacher Taschenrechner4Interfaces
Einführung in InterfacesImplizite ImplementierungInterface als VertragLeeres Interface (any)Type AssertionType SwitchInterface-KompositionStringer & Error InterfacesRückblick - Formen-Rechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf Paket-EbeneGetter- & Setter-MethodenInformation Hiding in GoZusammenfassung - Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterType ConstraintsGenerische StructsWorkaround für generische MethodenZusammenfassung – Generische Collection2Vertiefung: Typen & Structs
Basis- & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct TagsAnonyme StructsVerschachtelte StructsZero Values & StandardwerteZusammenfassung - Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen des Struct EmbeddingMethod PromotionEinbetten mehrerer StructsEmbedding vs. AggregationShadowing eingebetteter MethodenZusammenfassung – Mitarbeiter-Hierarchie8Fehlerbehandlung & OOP
Das error-InterfaceEigene FehlertypenError Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverZusammenfassung – Datei-Parser3Zeiger & Speicher
Zeiger-Grundlagen in GoZeiger auf StructsWertübergabe vs. ReferenzübergabeDie new() FunktionGarbage Collection in GoWiederholung – Linked List Builder6Polymorphismus in Go
Polymorphismus über InterfacesDuck Typing in GoInterface-ErfüllungsregelnPolymorphe CollectionsDependency InjectionZusammenfassung – Payment Processor9Nebenläufigkeit & OOP
Goroutines GrundlagenChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignZusammenfassung - Worker Pool