Menu
CoddyTech

Patrón Observer

Parte de la sección Programación orientada a objetos del Journey de GDScript de Coddy. Lección 48 de 65.

El patrón Observer permite que un objeto, el sujeto, les comunique a muchos otros objetos, los observadores, que ha ocurrido algo. El sujeto no sabe qué hacen los observadores con la noticia; solo la anuncia.

Escrito manualmente, el sujeto mantiene un arreglo de observadores y llama al mismo método en cada uno:

# 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 ya incluye este patrón. Una señal es el anuncio del sujeto, y cada connect() añade un observador, así que ya no se necesitan el array ni el bucle:

# 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)

Un observador es cualquier objeto con un método cuyos parámetros coinciden con la señal:

# 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 no tiene ni idea de que existe una barra o un registro. Un efecto de sonido o un logro también pueden escuchar, sin cambiar ni una sola cosa en health.gd.

Tres reglas hacen que los observadores sigan funcionando:

  • Conectar el mismo método dos veces es un error. Cuando pueda ocurrir, compruébalo primero con hp_changed.is_connected(bar.on_hp_changed). disconnect() elimina un observador, y desconectar uno que no está conectado también es un error.
  • Una conexión no mantiene vivo al observador. Guarda el observador en una variable o en un arreglo: un objeto creado dentro de la llamada a connect() se libera de inmediato y nunca recibe la señal.
  • Conecta métodos de los observadores. Una lambda que usa el sujeto y está conectada a la propia señal del sujeto lo mantiene vivo para siempre, lo que Godot informa como una fuga cuando se cierra el juego.
challenge icon

Desafío

Intermedio

Construye una estación meteorológica que notifique a sus pantallas mediante una señal.

En weather_station.gd:

  • Declara la señal temperature_changed(value: float).
  • attach(display) conecta el método on_temperature_changed de la pantalla. Si se conecta una pantalla que ya está conectada, no hace nada.
  • detach(display) la desconecta. Si se desconecta una pantalla que no está conectada, no hace nada.
  • set_temperature(value) almacena el valor en temperature y lo emite con la señal.

En weather_display.gd, on_temperature_changed(value) imprime el nombre de la pantalla y el valor con un decimal, como Phone: 21.5 C.

El código proporcionado lee los nombres de las pantallas en una línea, cuántos comandos siguen y luego un comando por línea: attach NAME, detach NAME o temp VALUE. Al final, imprime la última temperatura. Para los nombres Phone Tablet y los comandos attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19, el resultado es:

Phone: 21.5 C
Tablet: 21.5 C
Tablet: 19.0 C
Final temperature: 19.0

Tu código va en weather_station.gd y weather_display.gd. main.gd contiene el código de entrada/salida proporcionado y no se puede editar.

Pruébalo tú mismo

extends Node

func _ready():
	# Código de entrada/salida proporcionado: mantenlo tal como está
	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)
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación orientada a objetos

Practica por tu cuenta: Compilador de GDScript online