Interface Segregation Principle (ISP)
6.1 Vấn đề "Fat Interface" trong Game RPG
Nguyên lý Phân tách Interface (ISP) khuyến cáo rằng không nên ép buộc một lớp phải thực thi những interface mà nó không sử dụng. Trong các game nhập vai (RPG), xu hướng tạo ra các "Fat Interface" (Interface béo phì) rất phổ biến. Ví dụ, một interface IEntity có thể chứa các phương thức: Move(), Attack(), Heal(), Fly(), Trade().
Khi áp dụng IEntity cho một nhân vật Chiến binh (Warrior), lớp này buộc phải thực thi hàm Fly() và Heal() dù chiến binh không biết bay hay hồi máu. Điều này dẫn đến mã nguồn rỗng (Empty methods) hoặc ném ngoại lệ, gây nhiễu loạn logic và khó bảo trì.
6.2 Thách thức Serialization của Interface trong Unity
Một trong những rào cản lớn nhất khi áp dụng ISP và DIP trong Unity là hệ thống Serialization của Inspector không hỗ trợ hiển thị các biến có kiểu là Interface theo mặc định.
Ví dụ: public IAttack attacker; sẽ không hiển thị trường để kéo thả trong Inspector.
Để giải quyết vấn đề này và áp dụng ISP hiệu quả, ta cần hiểu rõ các giải pháp kỹ thuật:
Giải pháp 1: Sử dụng [SerializeReference] (Unity 2019.3+)
Thuộc tính này cho phép Unity serialize các trường tham chiếu đến các đối tượng C# thuần (không kế thừa MonoBehaviour) dưới dạng đa hình.
[SerializeReference]
public IAttack attackStrategy;
Tuy nhiên, giải pháp này có hạn chế về giao diện người dùng (UI) trong Editor, thường không hỗ trợ kéo thả trực quan các ScriptableObject hoặc MonoBehaviour thực thi interface đó một cách mượt mà như mong đợi. Ngoài ra, việc đổi tên lớp có thể làm mất dữ liệu tham chiếu nếu không sử dụng [MovedFromAttribute] để hướng dẫn Unity mapping lại dữ liệu cũ.
Giải pháp 2: Mẫu thiết kế Wrapper / Abstract Base Class
Một cách tiếp cận thực dụng hơn trong Unity là sử dụng lớp cơ sở trừu tượng (abstract class) kế thừa từ MonoBehaviour thay vì Interface thuần túy khi cần hiển thị trên Inspector.
// Thay vì interface IAttacker
public abstract class AttackerBase : MonoBehaviour {
public abstract void Attack();
}
Lớp AttackerBase sẽ hiển thị bình thường trên Inspector, cho phép kéo thả, trong khi vẫn đảm bảo tính trừu tượng cần thiết.
Giải pháp 3: GetComponent Validation
Giữ biến tham chiếu là MonoBehaviour hoặc GameObject trong Inspector, nhưng kiểm tra tính hợp lệ trong OnValidate hoặc Awake.
private MonoBehaviour attackerComponent;
private IAttacker _attacker;
void OnValidate() {
if (attackerComponent != null && !(attackerComponent is IAttacker)) {
Debug.LogError("Component phải thực thi interface IAttacker!");
attackerComponent = null;
}
}
Cách này đảm bảo tính toàn vẹn dữ liệu (Data Integrity) trong Editor mà không hy sinh lợi ích của Interface.