Beobachter-Muster
Teil des Abschnitts Objektorientierte Programmierung der GDScript-Journey von Coddy. Lektion 48 von 65.
Das Observer-Muster ermöglicht es einem Objekt, dem Subjekt, vielen anderen Objekten, den Beobachtern, mitzuteilen, dass etwas passiert ist. Das Subjekt weiß nicht, was die Beobachter mit dieser Nachricht machen; es gibt sie lediglich bekannt.
Manuell geschrieben, speichert das Subjekt ein Array von Beobachtern und ruft bei jedem dieselbe Methode auf:
# health.gd
class_name Health
extends RefCounted
var observers: Array = []
var hp := 100
func add_observer(observer) -> void:
observers.append(observer)
func take_damage(amount: int) -> void:
hp -= amount
for observer in observers:
observer.on_hp_changed(hp)Godot hat dieses Muster integriert. Ein Signal ist die Bekanntmachung des Subjekts, und jedes connect() fügt einen Beobachter hinzu, sodass das Array und die Schleife nicht mehr benötigt werden:
# health.gd
class_name Health
extends RefCounted
signal hp_changed(new_hp: int)
var hp := 100
func take_damage(amount: int) -> void:
hp -= amount
hp_changed.emit(hp)Ein Beobachter ist jedes Objekt mit einer Methode, deren Parameter mit dem Signal übereinstimmen:
# health_bar.gd
class_name HealthBar
extends RefCounted
func on_hp_changed(new_hp: int) -> void:
print("Bar: ", new_hp, "/100")# damage_log.gd
class_name DamageLog
extends RefCounted
func on_hp_changed(new_hp: int) -> void:
if new_hp < 50:
print("Log: low health!")# main.gd
var health := Health.new()
var bar := HealthBar.new()
var damage_log := DamageLog.new()
health.hp_changed.connect(bar.on_hp_changed)
health.hp_changed.connect(damage_log.on_hp_changed)
health.take_damage(70)Bar: 30/100
Log: low health!Health weiß nicht, dass es eine Leiste oder ein Protokoll gibt. Auch ein Soundeffekt oder ein Erfolg kann darauf reagieren, ohne dass health.gd auch nur einmal geändert werden muss.
Drei Regeln sorgen dafür, dass Beobachter funktionieren:
- Dieselbe Methode zweimal zu verbinden, ist ein Fehler. Wenn das passieren könnte, prüfe zuerst mit
hp_changed.is_connected(bar.on_hp_changed).disconnect()entfernt einen Beobachter, und auch das Trennen eines nicht verbundenen Beobachters ist ein Fehler. - Eine Verbindung hält den Beobachter nicht am Leben. Bewahre den Beobachter in einer Variablen oder einem Array auf: Ein Objekt, das innerhalb des Aufrufs
connect()erstellt wird, wird sofort freigegeben und empfängt das Signal nie. - Verbinde Methoden der Beobachter. Eine Lambda-Funktion, die das Subjekt verwendet und mit dem eigenen Signal des Subjekts verbunden ist, hält das Subjekt für immer am Leben, was Godot beim Schließen des Spiels als Speicherleck meldet.
Aufgabe
MittelErstelle eine Wetterstation, die ihre Displays mit einem Signal benachrichtigt.
In weather_station.gd:
- Deklariere das Signal
temperature_changed(value: float). attach(display)verbindet die Methodeon_temperature_changeddes Displays. Das Anfügen eines bereits angefügten Displays bewirkt nichts.detach(display)trennt die Verbindung. Das Entfernen eines nicht angefügten Displays bewirkt nichts.set_temperature(value)speichert den Wert intemperatureund sendet das Signal mit diesem Wert aus.
In weather_display.gd gibt on_temperature_changed(value) den Namen des Displays und den Wert mit einer Nachkommastelle aus, etwa Phone: 21.5 C.
Der bereitgestellte Code liest die Displaynamen in einer Zeile, anschließend die Anzahl der folgenden Befehle und dann einen Befehl pro Zeile: attach NAME, detach NAME oder temp VALUE. Am Ende gibt er die letzte Temperatur aus. Für die Namen Phone Tablet und die Befehle attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19 lautet die Ausgabe:
Phone: 21.5 C
Tablet: 21.5 C
Tablet: 19.0 C
Final temperature: 19.0Dein Code gehört in weather_station.gd und weather_display.gd. main.gd enthält den bereitgestellten Ein-/Ausgabecode und kann nicht bearbeitet werden.
Probier es selbst
extends Node
func _ready():
# Vorgegebener Eingabe-/Ausgabecode: unverändert lassen
var station := WeatherStation.new()
var displays := {}
for display_name in OS.read_string_from_stdin().split(" "):
displays[display_name] = WeatherDisplay.new(display_name)
var count := int(OS.read_string_from_stdin())
for i in count:
var parts := OS.read_string_from_stdin().split(" ")
match parts[0]:
"attach":
station.attach(displays[parts[1]])
"detach":
station.detach(displays[parts[1]])
"temp":
station.set_temperature(float(parts[1]))
print("Final temperature: %.1f" % station.temperature)
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
Externe DateienEinführung in die OOPKlassen vs. ObjekteDas Schlüsselwort selfMethodenEigenschaftenKonstruktor (_init)Wiederholung – Einfacher Taschenrechner4Polymorphie
Methodenüberschreibung erneut betrachtetDuck-TypisierungAbstrakte Klassen und MethodenSchnittstellendesignRückblick – Formrechner2Klassenattribute
Instanz- vs. statische VariablenStatische FunktionenProperty-SetterProperty-GetterPrivat nach KonventionWiederholung – Bankkontenverwaltung5Kapselung
Öffentliche und private MemberValidierung mit SetternInformationsverbergenSchreibgeschützte EigenschaftenRückblick – System zur Schülerverwaltung8Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungInnere KlassenNodes als KomponentenRefCounted vs. NodeEnums in Klassen11Projekt: Bibliotheksverwaltung
ProjektübersichtBuch- und Benutzerklassen3Vererbung
Grundlegende VererbungDas Schlüsselwort superMethodenüberschreibungVererbungskettenTypprüfungen mit isWiederholung – Mitarbeiterhierarchie6Spezielle Methoden
Einführung in virtuelle MethodenDie _to_string-MethodeBenutzerdefinierte IteratorenRückblick – Benutzerdefinierte Liste9Entwurfsmuster Teil 1
Einführung in EntwurfsmusterSingleton-MusterFactory-MusterBeobachter-MusterStrategie-Muster12Abschließende Herausforderungen
E-Learning-PlattformBankensystemEntwicklung von SpielfigurenFahrzeugvermietungÜbe selbstständig: Online-GDScript-Compiler