Menu
Coddy logo textTech

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.

challenge icon

Aufgabe

Einfach

Lassen 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 ein Notifier-Interface mit einer einzigen Methode Notify(message string) string. Erstellen Sie außerdem ein BaseNotifier-Struct mit einem Name-Feld, das von anderen Typen eingebettet wird. Geben Sie BaseNotifier eine Methode namens Format(message string) string, die die Nachricht mit dem Namen des Notifiers in Klammern vorangestellt zurückgibt.
  • channels.go: Erstellen Sie zwei Benachrichtigungskanal-Typen, die BaseNotifier einbetten und das Notifier-Interface implementieren:
    • EmailNotifier mit einem zusätzlichen Address-Feld
    • SMSNotifier mit einem zusätzlichen Phone-Feld
    Die Notify-Methode jedes Typs sollte die eingebettete Format-Methode verwenden und ihre spezifischen Kanalinformationen in die Ausgabe einbeziehen.
  • main.go: Erstellen Sie eine Funktion namens SendAlert, die einen beliebigen Notifier und eine Nachricht akzeptiert und dann das Ergebnis des Aufrufs von Notify zurü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] message

Ihre 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 down

Beachten 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
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung