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

Dependency Inversion Principle (DIP)

7.1 Từ Tight Coupling đến Dependency Injection

Nguyên lý Đảo ngược Phụ thuộc (DIP) là chìa khóa để phá vỡ sự phụ thuộc chặt chẽ. Nguyên lý này yêu cầu: "Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào sự trừu tượng".

Trong Unity, mô hình "Kéo thả trong Inspector" thực chất là một dạng sơ khai của Dependency Injection (DI) - cụ thể là Setter/Property Injection. Tuy nhiên, khi quy mô dự án lớn, việc kéo thả thủ công trở nên dễ sai sót và khó quản lý.

7.2 Phân tích Case Study: Hệ thống Công tắc (Switch) và Cửa (Door)

Xét ví dụ một hệ thống tương tác trong game giải đố:

Thiết kế Vi phạm DIP:

Lớp Switch (Công tắc) chứa tham chiếu trực tiếp đến lớp Door.

public class Switch : MonoBehaviour {
public Door door; // Phụ thuộc trực tiếp vào chi tiết cụ thể
public void Toggle() {
door.Open(); // hoặc door.Close()
}
}

Vấn đề phát sinh khi bạn muốn dùng công tắc này để bật một ngọn đèn (Light) hoặc kích hoạt thang máy (Elevator). Bạn không thể tái sử dụng Switch vì nó đã bị "hàn chết" vào Door.

Thiết kế Tuân thủ DIP (Sử dụng Interface):

Chúng ta định nghĩa một giao thức giao tiếp chung ISwitchable.

public interface ISwitchable {
void Activate();
void Deactivate();
}

public class Switch : MonoBehaviour {
// Phụ thuộc vào trừu tượng, không phụ thuộc vào Door hay Light
private ISwitchable _target;

// Sử dụng GetComponent để inject dependency tại runtime hoặc Awake
private MonoBehaviour _targetComponent;

void Awake() {
_target = _targetComponent as ISwitchable;
}

public void Toggle() {
_target.Activate();
}
}

Bây giờ, lớp Switch có thể điều khiển bất kỳ thứ gì: Cửa, Đèn, Bẫy, hay thậm chí là kích hoạt một đoạn Cutscene, miễn là đối tượng đó thực thi ISwitchable. Sự phụ thuộc đã được đảo ngược: thay vì Switch cần Door, cả SwitchDoor đều tuân thủ hợp đồng ISwitchable.

7.3 Framework Dependency Injection (DI) trong Unity

Đối với các dự án lớn, việc quản lý phụ thuộc thủ công (GetComponent hoặc Inspector drag-drop) trở nên quá tải. Các DI Framework như Zenject (Extenject) hoặc VContainer ra đời để giải quyết vấn đề này.

Các Framework này cung cấp một "Container" trung tâm. Bạn đăng ký các dịch vụ (Services) vào Container (ví dụ: AudioService, InventorySystem, NetworkManager). Khi một lớp cần sử dụng dịch vụ, nó chỉ cần khai báo trong Constructor (hoặc qua thuộc tính [Inject]), và Framework sẽ tự động cung cấp instance phù hợp.

Lợi ích của DI Framework:

  1. Loại bỏ Singleton: Giảm thiểu việc sử dụng Singleton Pattern (vốn bị coi là Anti-pattern trong nhiều trường hợp vì gây khó khăn cho Unit Test và tạo ra trạng thái toàn cục ẩn).

  2. Dễ dàng Mocking: Khi viết Unit Test, bạn có thể dễ dàng thay thế RealNetworkService bằng MockNetworkService để kiểm thử logic game mà không cần kết nối mạng thật.