Menu
CoddyTech

Mẫu Observer

Thuộc phần Lập trình hướng đối tượng trong hành trình GDScript của Coddy. Bài học 48 trên 65.

Mẫu Observer cho phép một đối tượng, gọi là chủ thể, thông báo cho nhiều đối tượng khác, gọi là các đối tượng quan sát, rằng có điều gì đó đã xảy ra. Chủ thể không biết các đối tượng quan sát sẽ làm gì với thông tin đó; nó chỉ thông báo.

Viết thủ công, đối tượng giữ một mảng các bên theo dõi và gọi cùng một phương thức trên từng bên:

# 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 tích hợp sẵn mẫu này. Tín hiệu là thông báo của đối tượng phát đi, còn mỗi lần gọi connect() sẽ thêm một đối tượng quan sát, nên bạn không cần mảng và vòng lặp nữa:

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

Một đối tượng lắng nghe là bất kỳ đối tượng nào có phương thức với các tham số khớp với tín hiệu:

# 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 không biết thanh trạng thái hay nhật ký có tồn tại. Hiệu ứng âm thanh hoặc thành tích cũng có thể lắng nghe mà không cần thay đổi health.gd.

Có ba quy tắc giúp các observer hoạt động ổn định:

  • Kết nối cùng một phương thức hai lần sẽ gây ra lỗi. Nếu có thể xảy ra tình huống này, hãy kiểm tra trước bằng hp_changed.is_connected(bar.on_hp_changed). disconnect() xóa một observer, và ngắt kết nối một observer chưa được kết nối cũng gây ra lỗi.
  • Kết nối không giữ cho observer tồn tại. Hãy giữ observer trong một biến hoặc một mảng: đối tượng được tạo bên trong lệnh gọi connect() sẽ bị giải phóng ngay lập tức và không bao giờ nhận được tín hiệu.
  • Hãy kết nối các phương thức của observer. Một lambda sử dụng đối tượng phát tín hiệu và được kết nối với chính tín hiệu của đối tượng đó sẽ khiến đối tượng phát tín hiệu tồn tại mãi mãi. Godot sẽ báo đây là một rò rỉ khi trò chơi đóng.
challenge icon

Thử thách

Trung bình

Xây dựng một trạm thời tiết dùng tín hiệu để thông báo cho các màn hình.

Trong weather_station.gd:

  • Khai báo tín hiệu temperature_changed(value: float).
  • attach(display) kết nối phương thức on_temperature_changed của màn hình. Gắn một màn hình đã được gắn rồi sẽ không làm gì.
  • detach(display) ngắt kết nối màn hình đó. Gỡ một màn hình chưa được gắn sẽ không làm gì.
  • set_temperature(value) lưu giá trị vào temperature và phát tín hiệu kèm theo giá trị đó.

Trong weather_display.gd, on_temperature_changed(value) in ra tên màn hình và giá trị với một chữ số thập phân, như Phone: 21.5 C.

Code được cung cấp đọc tên các màn hình trên một dòng, số lượng lệnh tiếp theo, rồi đến mỗi lệnh trên một dòng: attach NAME, detach NAME hoặc temp VALUE. Cuối cùng, chương trình in ra nhiệt độ cuối cùng. Với các tên Phone Tablet và các lệnh attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19, đầu ra là:

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

Code của bạn nằm trong weather_station.gd và weather_display.gd. main.gd chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

extends Node

func _ready():
	# Đoạn code đầu vào/đầu ra có sẵn: giữ nguyên
	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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch GDScript online