Menu
CoddyTech

Padrão Observer

Parte da seção Programação orientada a objetos do Journey de GDScript da Coddy. Lição 48 de 65.

O padrão Observer permite que um objeto, o sujeito, informe a muitos outros objetos, os observadores, que algo aconteceu. O sujeito não sabe o que os observadores fazem com a notícia; ele apenas a anuncia.

Escrito manualmente, o sujeito mantém um array de observadores e chama o mesmo método em cada um:

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

O Godot já tem esse padrão integrado. Um sinal é o anúncio do sujeito, e cada connect() adiciona um observador, então o array e o loop não são mais necessários:

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

Um observador é qualquer objeto que tenha um método cujos parâmetros correspondam ao sinal:

# 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 não faz ideia de que existe uma barra ou um registro. Um efeito sonoro ou uma conquista também pode escutar, sem que seja necessária uma única alteração em health.gd.

Três regras mantêm os observadores funcionando:

  • Conectar o mesmo método duas vezes é um erro. Quando isso puder acontecer, verifique primeiro com hp_changed.is_connected(bar.on_hp_changed). disconnect() remove um observador, e desconectar um que não está conectado também é um erro.
  • Uma conexão não mantém o observador vivo. Mantenha o observador em uma variável ou em um array: um objeto criado dentro da chamada connect() é liberado imediatamente e nunca recebe o sinal.
  • Conecte métodos dos observadores. Uma lambda que usa o sujeito e é conectada ao próprio sinal do sujeito mantém o sujeito vivo para sempre, o que Godot informa como um vazamento quando o jogo é fechado.
challenge icon

Desafio

Médio

Crie uma estação meteorológica que notifica seus visores com um sinal.

Em weather_station.gd:

  • Declare o sinal temperature_changed(value: float).
  • attach(display) conecta o método on_temperature_changed do visor. Anexar um visor que já está anexado não faz nada.
  • detach(display) o desconecta. Desanexar um visor que não está anexado não faz nada.
  • set_temperature(value) armazena o valor em temperature e o emite com o sinal.

Em weather_display.gd, on_temperature_changed(value) imprime o nome do visor e o valor com uma casa decimal, como Phone: 21.5 C.

O código fornecido lê os nomes dos visores em uma linha, quantos comandos vêm em seguida e, depois, um comando por linha: attach NAME, detach NAME ou temp VALUE. No final, imprime a última temperatura. Para os nomes Phone Tablet e os comandos attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19, a saída é:

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

Seu código deve ficar em weather_station.gd e weather_display.gd. main.gd contém o código fornecido de entrada/saída e não pode ser editado.

Experimente você mesmo

extends Node

func _ready():
	# Código de entrada/saída fornecido: mantenha-o 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 iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação orientada a objetos

Pratique por conta própria: Compilador de GDScript online