Adapter-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 92 von 107.
Das Adapter-Muster ermöglicht es inkompatiblen Schnittstellen zusammenzuarbeiten, indem ein bestehender Typ mit einer neuen Schnittstelle umhüllt wird. Während Command Aktionen kapselt, übersetzt Adapter eine Schnittstelle in eine andere, die Clients erwarten.
Stellen Sie sich vor, Sie haben einen veralteten Drucker, der eine andere Methodensignatur verwendet, als Ihre Anwendung erwartet:
// Ziel-Interface, das Ihre Anwendung verwendet
type Printer interface {
Print(text string) string
}
// Veralteter Drucker mit inkompatiblem Interface
type OldPrinter struct{}
func (o OldPrinter) PrintDocument(doc string, copies int) string {
return fmt.Sprintf("Old printer: %s (x%d)", doc, copies)
}Der Adapter kapselt den veralteten Typ und implementiert die erwartete Schnittstelle:
type OldPrinterAdapter struct {
oldPrinter OldPrinter
}
func (a OldPrinterAdapter) Print(text string) string {
return a.oldPrinter.PrintDocument(text, 1)
}Jetzt arbeitet der Legacy-Drucker nahtlos mit Code zusammen, der das Printer-Interface erwartet:
func PrintMessage(p Printer, msg string) string {
return p.Print(msg)
}
adapter := OldPrinterAdapter{oldPrinter: OldPrinter{}}
result := PrintMessage(adapter, "Hello") // "Old printer: Hello (x1)"Das Adapter-Muster ist von unschätzbarem Wert bei der Integration von Bibliotheken von Drittanbietern, bei der Arbeit mit Legacy-Code oder wenn Sie vorhandene Klassen verwenden müssen, die nicht Ihren Schnittstellenanforderungen entsprechen. Es fungiert als Brücke, ohne den ursprünglichen Code zu ändern.
Aufgabe
EinfachLassen Sie uns ein Media-Player-System mit dem Adapter-Muster bauen! Sie haben einen modernen Audioplayer, der eine einheitliche Schnittstelle erwartet, aber Sie müssen veraltete Audiogeräte integrieren, die völlig andere Methodensignaturen verwenden. Ihr Adapter wird diese Lücke schließen und es der alten Hardware ermöglichen, nahtlos mit dem neuen System zusammenzuarbeiten.
Sie werden Ihren Code über drei Dateien organisieren:
player.go: Definieren Sie Ihr Ziel-Interface und ein veraltetes Audiogerät.Erstellen Sie ein
AudioPlayer-Interface, das Ihr modernes System erwartet, mit einer MethodePlay(track string) string, die einen Tracknamen entgegennimmt und eine Wiedergabenachricht zurückgibt.Definieren Sie außerdem ein veraltetes Gerät, das nicht zu diesem Interface passt:
VinylPlayer-Struktur mit einemSpeedRPM-Feld (int) — sie hat eine MethodeDropNeedle(record string, side string) string, diePlaying [record] ([side] side) at [speed]rpmzurückgibt.
Das Interface des VinylPlayer ist inkompatibel mit AudioPlayer — es erfordert zwei Parameter und verwendet einen völlig anderen Methodennamen.
adapter.go: Erstellen Sie Ihren Adapter, der das veraltete Gerät kompatibel macht.Erstellen Sie eine
VinylAdapter-Struktur, die einenVinylPlayerkapselt und dasAudioPlayer-Interface implementiert. WennPlayaufgerufen wird, sollte der Adapter dieDropNeedle-Methode des Vinyl-Players aufrufen und dabei standardmäßig"A"für den Parameter side verwenden.Erstellen Sie zum Vergleich auch einen modernen Player:
DigitalPlayer-Struktur — ihrePlay-Methode gibtStreaming: [track]zurück.
main.go: Demonstrieren Sie, wie beide Player über dasselbe Interface funktionieren.Erstellen Sie eine Funktion
PlayMusic(player AudioPlayer, track string) string, die das Interface zum Abspielen von Musik verwendet — diese Funktion sollte nicht wissen oder sich darum kümmern, ob sie einen digitalen Player oder einen adaptierten Vinyl-Player verwendet.Lesen Sie einen Player-Typ (
"digital"oder"vinyl") ein, und falls es vinyl ist, lesen Sie auch die RPM-Geschwindigkeit ein. Lesen Sie dann den Tracknamen ein. Erstellen Sie den entsprechenden Player (unter Verwendung des Adapters für Vinyl), spielen Sie den Track über IhrePlayMusic-Funktion ab und geben Sie das Ergebnis aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Player-Typ (
"digital"oder"vinyl") - Zeile 2: RPM-Geschwindigkeit (nur bei Vinyl)
- Letzte Zeile: Name des abzuspielenden Tracks
Zum Beispiel, gegeben:
digital
Bohemian RhapsodyIhre Ausgabe sollte sein:
Streaming: Bohemian RhapsodyUnd gegeben:
vinyl
33
Abbey RoadIhre Ausgabe sollte sein:
Playing Abbey Road (A side) at 33rpmUnd gegeben:
vinyl
45
Blue MondayIhre Ausgabe sollte sein:
Playing Blue Monday (A side) at 45rpmBeachten Sie, wie die Funktion PlayMusic mit beiden Playern identisch funktioniert — sie hat keine Ahnung, dass der Vinyl-Player eigentlich von einem völlig anderen Interface adaptiert wird. Der Adapter übersetzt die modernen Interface-Aufrufe hinter den Kulissen in das veraltete Format!
Spickzettel
Das Adapter-Muster ermöglicht es inkompatiblen Schnittstellen zusammenzuarbeiten, indem ein bestehender Typ mit einer neuen Schnittstelle umhüllt wird.
Definieren Sie eine Ziel-Schnittstelle (Target Interface), die Ihre Anwendung erwartet:
type Printer interface {
Print(text string) string
}Ein veralteter Typ (Legacy Type) mit einer inkompatiblen Schnittstelle:
type OldPrinter struct{}
func (o OldPrinter) PrintDocument(doc string, copies int) string {
return fmt.Sprintf("Old printer: %s (x%d)", doc, copies)
}Erstellen Sie einen Adapter, der den veralteten Typ umhüllt und die Ziel-Schnittstelle implementiert:
type OldPrinterAdapter struct {
oldPrinter OldPrinter
}
func (a OldPrinterAdapter) Print(text string) string {
return a.oldPrinter.PrintDocument(text, 1)
}Verwenden Sie den Adapter, damit der veraltete Typ mit Code funktioniert, der die Ziel-Schnittstelle erwartet:
func PrintMessage(p Printer, msg string) string {
return p.Print(msg)
}
adapter := OldPrinterAdapter{oldPrinter: OldPrinter{}}
result := PrintMessage(adapter, "Hello") // "Old printer: Hello (x1)"Das Adapter-Muster ist nützlich für die Integration von Drittanbieter-Bibliotheken, die Arbeit mit Legacy-Code oder die Anpassung bestehender Klassen an erforderliche Schnittstellen, ohne den ursprünglichen Code zu ändern.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// PlayMusic verwendet das AudioPlayer-Interface, um Musik abzuspielen
// Diese Funktion arbeitet mit jeder AudioPlayer-Implementierung
// TODO: Implementiere diese Funktion, um player.Play(track) aufzurufen und das Ergebnis zurückzugeben
func PlayMusic(player AudioPlayer, track string) string {
// TODO: Implementiere diese Funktion
return ""
}
func main() {
reader := bufio.NewReader(os.Stdin)
// Player-Typ einlesen
playerType, _ := reader.ReadString('\n')
playerType = strings.TrimSpace(playerType)
var player AudioPlayer
var track string
if playerType == "vinyl" {
// RPM-Geschwindigkeit für Vinyl einlesen
rpmStr, _ := reader.ReadString('\n')
rpmStr = strings.TrimSpace(rpmStr)
rpm, _ := strconv.Atoi(rpmStr)
// Track-Namen einlesen
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO: Erstelle einen VinylPlayer mit der angegebenen RPM
// TODO: Hülle ihn in einen VinylAdapter ein
// TODO: Der Variable player zuweisen
_ = rpm // Entferne diese Zeile, wenn du rpm verwendest
} else {
// Track-Namen einlesen
track, _ = reader.ReadString('\n')
track = strings.TrimSpace(track)
// TODO: Erstelle einen DigitalPlayer
// TODO: Der Variable player zuweisen
}
// TODO: Rufe PlayMusic auf und gib das Ergebnis aus
_ = player // Entferne diese Zeile, wenn du player verwendest
_ = track // Entferne diese Zeile, wenn du track verwendest
}
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-Parser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json mit Structshttp.Handler InterfaceZusammenfassung - REST-API-Modelle14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterMiddleware als Decorator3Zeiger & 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