Observer-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 89 von 107.
Das Observer-Muster definiert eine Eins-zu-viele-Beziehung zwischen Objekten. Wenn ein Objekt (das Subjekt) seinen Zustand ändert, werden alle seine Abhängigen (Beobachter) automatisch benachrichtigt. Dies ist ideal für Ereignissysteme, Benachrichtigungen oder jedes Szenario, in dem mehrere Komponenten auf Änderungen reagieren müssen.
In Go implementieren wir dieses Muster mithilfe eines Interfaces für Observer und eines Subject-Structs, das Abonnements verwaltet:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Jeder Typ, der das Observer-Interface implementiert, kann sich anmelden, um Benachrichtigungen zu erhalten:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}Das Subjekt benachrichtigt alle Beobachter, ohne deren konkrete Typen zu kennen:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// E-Mail an user@example.com: Order placed
// Log: Order placedDieses Muster entkoppelt das Subjekt von seinen Beobachtern, wodurch es einfach wird, neue Beobachtertypen hinzuzufügen, ohne den bestehenden Code zu ändern. Es wird häufig in ereignisgesteuerten Architekturen, GUI-Frameworks und Pub/Sub-Messaging-Systemen verwendet.
Aufgabe
EinfachLass uns ein Aktienpreis-Alarmsystem mit dem Observer-Pattern bauen! Du wirst ein System erstellen, bei dem mehrere Alarmdienste automatisch Benachrichtigungen erhalten, wann immer sich ein Aktienkurs ändert – perfekt, um zu demonstrieren, wie Observer auf Ereignisse reagieren können, ohne dass das Subjekt deren konkrete Typen kennen muss.
Du wirst deinen Code über drei Dateien organisieren:
observer.go: Definiere dein Observer-Interface und die konkreten Observer-Typen.Erstelle ein
Observer-Interface mit einer MethodeUpdate(stockName string, price float64), die Aktieninformationen erhält, wenn sich die Preise ändern.Implementiere zwei Observer-Typen:
PriceAlertmit einemThreshold-Feld (float64) – wenn es aktualisiert wird, gibt esALERT: [stockName] at $[price] crossed threshold $[threshold]zurück, falls der Preis über dem Schwellenwert liegt, oderWATCHING: [stockName] at $[price] (threshold: $[threshold]), falls er darunter liegt.PriceLoggermit einemLogName-Feld (string) – wenn es aktualisiert wird, gibt es[logName] recorded [stockName]: $[price]zurück.
Beide Observer sollten eine Methode haben, die ihren formatierten Antwort-String zurückgibt.
stock.go: Erstelle dein Subjekt, das die Observer verwaltet und sie über Preisänderungen benachrichtigt.Erstelle ein
Stock-Struct mit Feldern fürName(string),Price(float64) und einem Slice von Observern. Füge diese Methoden hinzu:Subscribe(o Observer), um einen Observer hinzuzufügenSetPrice(price float64) []string, das den Preis aktualisiert, alle Observer benachrichtigt und ein Slice mit all ihren Antworten zurückgibt
Erstelle außerdem einen
NewStock(name string, initialPrice float64) *Stock-Konstruktor.main.go: Richte deine Aktie mit Observern ein und simuliere Preisänderungen.Lies den Aktiennamen und den Anfangspreis ein. Lies dann die Anzahl der zu erstellenden Observer ein. Lies für jeden Observer seinen Typ (
alertoderlogger) und seine Konfiguration (Schwellenwert für Alerts, Log-Name für Loggers) ein. Abonniere jeden Observer bei der Aktie.Lies schließlich einen neuen Preis ein, aktualisiere die Aktie und gib die Antwort jedes Observers in einer eigenen Zeile aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Aktienname
- Zeile 2: Anfangspreis
- Zeile 3: Anzahl der Observer
- Für jeden Observer: Typ (
alertoderlogger), dann Konfigurationswert - Letzte Zeile: Neuer zu setzender Preis
Formatiere alle Preise mit zwei Dezimalstellen.
Zum Beispiel, gegeben:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50Deine Ausgabe sollte sein:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00Und gegeben:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25Deine Ausgabe sollte sein:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Beachte, dass die Aktie (Stock) nicht weiß, ob sie Alerts oder Loggers benachrichtigt – sie ruft einfach Update für jeden Observer auf. Diese Entkopplung macht es einfach, neue Observer-Typen hinzuzufügen, ohne den Code von Stock zu ändern!
Spickzettel
Das Observer-Muster definiert eine Eins-zu-viele-Beziehung, bei der ein Subjekt mehrere Beobachter automatisch benachrichtigt, wenn sich sein Zustand ändert. Dies entkoppelt das Subjekt von seinen Beobachtern und macht es ideal für Ereignissysteme und Benachrichtigungen.
Grundstruktur
Definieren Sie ein Observer-Interface und ein Subjekt, das Abonnements verwaltet:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Implementierung von Objekten
Jeder Typ, der das Observer-Interface implementiert, kann sich anmelden:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}Verwendung
Melden Sie Beobachter an und benachrichtigen Sie diese über Änderungen:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedDas Subjekt benachrichtigt alle Beobachter, ohne deren konkrete Typen zu kennen, was es einfach macht, neue Beobachtertypen hinzuzufügen, ohne bestehenden Code zu ändern.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Aktiennamen lesen
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// Anfangspreis lesen
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// Anzahl der Beobachter lesen
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: Erstelle eine neue Stock mit dem NewStock-Konstruktor
// TODO: Durchlaufe jeden Beobachter in einer Schleife
for i := 0; i < numObservers; i++ {
// Beobachtertyp lesen ("alert" oder "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// Konfigurationswert lesen
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: Erstelle den passenden Beobachter basierend auf dem Typ
// - Für "alert": configValue als float64-Schwellenwert parsen, PriceAlert erstellen
// - Für "logger": configValue als LogName verwenden, PriceLogger erstellen
// TODO: Melde den Beobachter bei der Aktie an
}
// Neuen Preis lesen
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: Setze den neuen Preis und erhalte die Antworten
// TODO: Gib jede Antwort in einer eigenen Zeile aus
}
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 Collection13Entwurfsmuster Teil 1
Einführung in EntwurfsmusterSingleton-MusterFactory-MusterAbstract-Factory-MusterObserver-MusterStrategy-Muster2Vertiefung: 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