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

Open/Closed Principle (OCP)

4.1 Cơ sở Lý thuyết: Mở rộng vs Sửa đổi

Nguyên lý Đóng/Mở (Open/Closed Principle - OCP) khẳng định rằng các thực thể phần mềm nên mở cho việc mở rộng nhưng đóng đối với việc sửa đổi. Trong phát triển game, điều này có nghĩa là việc thêm một loại vũ khí mới, một kẻ thù mới, hay một kỹ năng (skill) mới không bao giờ được phép yêu cầu lập trình viên phải mở các file mã nguồn cốt lõi đã tồn tại để chỉnh sửa các câu lệnh điều kiện if/else hoặc switch/case.

4.2 Cạm bẫy của EnumSwitch Case

Một mô hình phổ biến vi phạm OCP trong Unity là việc sử dụng Enum để định nghĩa loại đối tượng, sau đó dùng switch để xử lý logic.

Mã nguồn Vi phạm OCP:

public class Weapon : MonoBehaviour {
public enum Type { Sword, Bow, MagicWand }
public Type weaponType;

public void Attack() {
switch (weaponType) {
case Type.Sword:
// Logic chém kiếm
break;
case Type.Bow:
// Logic bắn cung
break;
case Type.MagicWand:
// Logic bắn phép
break;
}
}
}

Mỗi khi Game Designer muốn thêm một loại vũ khí mới (ví dụ: Hammer), lập trình viên buộc phải: (1) Thêm giá trị vào Enum, (2) Thêm case vào hàm Attack, (3) Có thể phải thêm case vào các hàm khác như GetSound(), GetAnimation(). Quy trình này vi phạm OCP và tăng nguy cơ gây lỗi hồi quy (regression bugs) cho các loại vũ khí cũ.

4.3 Giải pháp Kiến trúc: Đa hình và ScriptableObject

Để giải quyết vấn đề này, Unity cung cấp hai công cụ mạnh mẽ: Kế thừa đa hình (Polymorphism) và ScriptableObject.

Phương pháp 1: Đa hình (Polymorphism)

Tạo lớp trừu tượng BaseWeapon và các lớp con Sword, Bow. Lớp điều khiển nhân vật chỉ gọi currentWeapon.Attack() mà không cần biết đó là loại vũ khí gì.

Phương pháp 2: ScriptableObject (Chiến lược Dữ liệu hóa Logic)

Đây là phương pháp đặc thù và mạnh mẽ nhất của Unity. Thay vì viết logic trong class, ta có thể đóng gói dữ liệu và thậm chí là hành vi vào ScriptableObject.

// Định nghĩa một hành vi tấn công trừu tượng
public abstract class AttackStrategy : ScriptableObject {
public abstract void ExecuteAttack(Transform origin);
}

// Tạo Asset cho kiểu tấn công bắn đạn (Projectile)
[CreateAssetMenu(menuName = "Attack/Projectile")]
public class ProjectileAttack : AttackStrategy {
public GameObject projectilePrefab;
public float force = 1000f;

public override void ExecuteAttack(Transform origin) {
var bullet = Instantiate(projectilePrefab, origin.position, origin.rotation);
bullet.GetComponent<Rigidbody>().AddForce(origin.forward * force);
}
}

// Tạo Asset cho kiểu tấn công tức thời (Raycast)
public class RaycastAttack : AttackStrategy {
public float range = 50f;
public LayerMask hitLayers;

public override void ExecuteAttack(Transform origin) {
if (Physics.Raycast(origin.position, origin.forward, out RaycastHit hit, range, hitLayers)) {
// Xử lý trúng đích
}
}
}

// Lớp Weapon tuân thủ OCP
public class WeaponController : MonoBehaviour {
// Designer có thể kéo thả bất kỳ chiến thuật tấn công nào vào đây
public AttackStrategy currentAttackStrategy;

public void Fire() {
if (currentAttackStrategy != null) {
currentAttackStrategy.ExecuteAttack(transform);
}
}
}

Với kiến trúc trên, khi cần thêm một kiểu tấn công mới (ví dụ: ném lựu đạn), lập trình viên chỉ cần tạo một class mới kế thừa AttackStrategy. Lớp WeaponController hoàn toàn không bị thay đổi (Đóng với sửa đổi). Game Designer có thể tạo ra hàng trăm biến thể vũ khí chỉ bằng cách tạo Asset trong Project window và gán các thông số khác nhau mà không cần viết code. Đây là sự thể hiện cao nhất của tính linh hoạt trong OCP.