Chuyển tới nội dung chính

Single Responsibility Principle (SRP)

3.1 Lý thuyết và Ứng dụng trong ECS/Component Model

Nguyên lý Đơn nhiệm (Single Responsibility Principle - SRP) quy định rằng "Một lớp chỉ nên có một lý do duy nhất để thay đổi". Trong Unity, nguyên lý này cộng hưởng mạnh mẽ với mô hình Entity-Component-System (ECS) hoặc mô hình Component truyền thống. Một GameObject (Entity) nên được cấu thành từ nhiều MonoBehaviour nhỏ (Components), mỗi component đảm nhận một trách nhiệm riêng biệt.

Thay vì một lớp Player khổng lồ, SRP khuyến khích việc phân rã thành: PlayerInput (đọc dữ liệu điều khiển), PlayerLocomotion (xử lý vật lý di chuyển), PlayerAudio (phản hồi âm thanh), và PlayerStats (quản lý dữ liệu sống).

3.2 Chiến lược Tái cấu trúc (Refactoring) PlayerController

Quá trình chuyển đổi từ "God Class" sang kiến trúc tuân thủ SRP đòi hỏi sự hiểu biết sâu sắc về luồng dữ liệu. Dưới đây là phân tích chi tiết về quy trình này:

Giai đoạn 1: Xác định Trách nhiệm

Phân tích lớp PlayerController hiện tại và nhóm các biến/phương thức theo chức năng. Ví dụ: moveSpeed, jumpForce, Rigidbody thuộc về nhóm Di chuyển; maxHealth, currentHealth, TakeDamage() thuộc về nhóm Sinh tồn.

Giai đoạn 2: Tách lớp và Thiết lập Giao tiếp

Việc tách lớp dẫn đến thách thức mới: Làm thế nào các lớp nhỏ này giao tiếp với nhau? Ví dụ, PlayerAudio cần biết khi nào PlayerLocomotion thực hiện cú nhảy để phát âm thanh. Có hai cách tiếp cận chính:

  1. Polling (Hỏi thăm): PlayerAudio giữ tham chiếu đến PlayerLocomotion và kiểm tra trạng thái trong Update(). Cách này đơn giản nhưng tạo ra sự phụ thuộc.

  2. Event-Driven (Hướng sự kiện): Đây là cách tiếp cận ưu việt hơn để duy trì SRP. PlayerLocomotion sẽ phát ra một sự kiện (C# event hoặc UnityEvent) khi nhân vật nhảy. PlayerAudio đăng ký lắng nghe sự kiện này. Như vậy, PlayerLocomotion không cần biết PlayerAudio có tồn tại hay không, hoàn toàn tuân thủ tính đóng gói.

Ví dụ Mã nguồn Tái cấu trúc:

// 1. Component chỉ chịu trách nhiệm nhận Input
public class InputHandler : MonoBehaviour {
// Sử dụng C# Action để các component khác đăng ký lắng nghe
public event Action<Vector2> OnMoveInput;
public event Action OnJumpPressed;

void Update() {
Vector2 input = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
OnMoveInput?.Invoke(input); // Phát sự kiện di chuyển

if (Input.GetKeyDown(KeyCode.Space)) {
OnJumpPressed?.Invoke(); // Phát sự kiện nhảy
}
}
}

// 2. Component chỉ chịu trách nhiệm Vật lý/Di chuyển
public class LocomotionHandler : MonoBehaviour {
private float speed = 5f;
private float jumpForce = 5f;
private Rigidbody _rb;
private InputHandler _inputHandler;

void Awake() {
_rb = GetComponent<Rigidbody>();
_inputHandler = GetComponent<InputHandler>();
}

void OnEnable() {
// Đăng ký lắng nghe sự kiện từ InputHandler
_inputHandler.OnMoveInput += Move;
_inputHandler.OnJumpPressed += Jump;
}

void OnDisable() {
// Hủy đăng ký để tránh rò rỉ bộ nhớ (Memory Leak)
_inputHandler.OnMoveInput -= Move;
_inputHandler.OnJumpPressed -= Jump;
}

private void Move(Vector2 direction) {
// Logic di chuyển vật lý
Vector3 moveForce = new Vector3(direction.x, 0, direction.y) * speed;
_rb.AddForce(moveForce);
}

private void Jump() {
// Logic nhảy
_rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}

3.3 Phân tích Sâu: SRP và Inspector UI

Một tác dụng phụ tích cực của SRP trong Unity là làm sạch giao diện Inspector. Thay vì một component dài dằng dặc với hàng trăm biến số rối rắm, Inspector giờ đây hiển thị các khối chức năng rõ ràng. Điều này giúp các Game Designer dễ dàng tinh chỉnh (tweak) thông số. Họ có thể vô hiệu hóa (disable) component PlayerAudio để tắt âm thanh nhân vật mà không cần nhờ lập trình viên can thiệp vào mã nguồn, điều không thể làm được nếu logic âm thanh nằm chung trong PlayerController.

Tuy nhiên, cần cảnh giác với việc phân mảnh quá mức (Over-fragmentation). Nếu một thực thể game có tới 50 component nhỏ li ti, việc quản lý trong Editor sẽ trở nên khó khăn và chi phí hiệu năng của việc gọi 50 hàm Update() (Native C++ sang Managed C# call overhead) có thể bắt đầu ảnh hưởng, dù Unity đã tối ưu hóa điều này khá tốt.