Schnittstelle als Vertrag
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 27 von 107.
Schnittstellen als Verträge zu betrachten, hilft dir dabei, bessere Go-Programme zu entwerfen. Wenn eine Funktion einen Schnittstellentyp akzeptiert, sagt sie damit: „Es ist mir egal, welchen konkreten Typ du mir gibst, solange er diesen Vertrag erfüllt.“
Dieses vertragsbasierte Denken ermöglicht es dir, Funktionen zu schreiben, die sich auf das Verhalten statt auf bestimmte Typen konzentrieren. Betrachte eine Funktion, die Daten aus verschiedenen Quellen verarbeiten muss:
type DataSource interface {
FetchData() string
}
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}Die Funktion ProcessData weiß nicht und kümmert sich nicht darum, ob sie eine Datenbankverbindung, einen API-Client oder einen Datei-Leser erhält. Sie benötigt lediglich, dass die Eingabe FetchData() aufrufen kann. Jeder Typ, der diesen Vertrag erfüllt, funktioniert.
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Funktioniert!
ProcessData(api) // Funktioniert auch!
}Dieser Vertragsansatz macht deinen Code außerdem besser testbar. Während des Testens kannst du einen Mock-Typ erstellen, der die Schnittstelle erfüllt, ohne echte Datenbanken oder Netzwerkverbindungen zu benötigen. Die zu testende Funktion kennt den Unterschied nicht, weil der Vertrag erfüllt ist.
Aufgabe
EinfachErstellen wir ein Benachrichtigungssystem, das zeigt, wie Schnittstellen als Verträge fungieren. Du erstellst eine Notifier-Schnittstelle, die definiert, was das Senden einer Benachrichtigung bedeutet, und implementierst anschließend mehrere Benachrichtigungskanäle, die alle diesen Vertrag einhalten.
Du organisierst deinen Code über zwei Dateien:
notifiers.go: Definiere eineNotifier-Schnittstelle mit einer einzigen MethodeNotify(message string) string. Erstelle anschließend drei verschiedene Strukturen, die diesen Vertrag erfüllen:ConsoleNotifiermit einemPrefix-FeldFileNotifiermit einemFilename-FeldWebhookNotifiermit einemURL-Feld
Notifyimplementieren, um eine Zeichenkette zurückzugeben, die beschreibt, wie die Benachrichtigung gesendet würde.main.go: Erstelle eine Funktion namensBroadcastAlert, die ein Slice vonNotifier-Werten und eine Nachrichtenzeichenkette entgegennimmt. Diese Funktion sollte für jeden NotifierNotifyaufrufen und die Ergebnisse ausgeben. Lies die Konfiguration aus der Eingabe, erstelle alle drei Notifier-Typen und sende eine Benachrichtigung über alle drei.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Präfix der Konsole
- Zeile 2: Dateiname
- Zeile 3: Webhook-URL
- Zeile 4: Benachrichtigungstext
Deine Notify-Methoden sollten Zeichenketten in diesen Formaten zurückgeben:
- ConsoleNotifier:
[Prefix] [message] - FileNotifier:
Writing to [Filename]: [message] - WebhookNotifier:
POST [URL]: [message]
Wenn beispielsweise ALERT:, log.txt, https://hooks.example.com und Server down! gegeben sind, sollte deine Ausgabe folgendermaßen aussehen:
ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!Die Stärke dieses Entwurfs besteht darin, dass deine Funktion BroadcastAlert die spezifischen Notifier-Typen weder kennt noch sich um sie kümmern muss. Sie verlangt lediglich, dass jeder einzelne den Notifier-Vertrag erfüllt. Du könntest später einen SlackNotifier oder EmailNotifier hinzufügen, ohne die Broadcast-Funktion überhaupt zu ändern.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
// BroadcastAlert sendet eine Nachricht über alle Notifier
// TODO: Implementiere diese Funktion, um:
// - einen Slice von Notifier-Werten und einen Nachrichten-String zu akzeptieren
// - Notify auf jedem Notifier aufzurufen und die Ergebnisse auszugeben
func BroadcastAlert(notifiers []Notifier, message string) {
// TODO: Gehe die Notifier durch und gib jedes Benachrichtigungsergebnis aus
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Konsolen-Präfix lesen
scanner.Scan()
prefix := scanner.Text()
// Dateiname lesen
scanner.Scan()
filename := scanner.Text()
// Webhook-URL lesen
scanner.Scan()
url := scanner.Text()
// Alert-Nachricht lesen
scanner.Scan()
message := scanner.Text()
// TODO: Erstelle Instanzen aller drei Notifier-Typen
// - ConsoleNotifier mit dem Präfix
// - FileNotifier mit dem Dateinamen
// - WebhookNotifier mit der URL
// TODO: Erstelle einen Slice von Notifier, der alle drei Notifier enthält
// TODO: Rufe BroadcastAlert mit den Notifiers und der Nachricht auf
_ = prefix
_ = filename
_ = url
_ = message
fmt.Println("TODO: Implement the solution")
}
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