← SOLID Principles

Bài 3 · Nâng cao · 20 phút· Cập nhật 11/06/2026

O - Open/Closed

Biên soạn bởi Nguyễn Anh Tuấn

Open/Closed Principle (OCP): hạn chế sửa đổi, ưu tiên mở rộng bằng interface để thêm tính năng mà không phá vỡ codebase hiện tại

Open/Closed Principle (OCP): một module nên mở để mở rộng (thêm hành vi mới) nhưng đóng để sửa đổi (không phải mổ lại code cũ đang chạy). Xem một hàm tính diện tích rẽ theo "loại":

❌ Vi phạm OCP - mỗi hình mới lại sửa hàm này

type Shape =
  | { kind: "square"; side: number }
  | { kind: "rectangle"; w: number; h: number };

function getArea(s: Shape): number {
  switch (s.kind) {
    case "square":    return s.side * s.side;
    case "rectangle": return s.w * s.h;
    // Them hinh Tron? Phai MO lai ham nay, them mot case nua...
    // ...va moi noi khac dang switch theo kind cung phai sua theo.
  }
}
  • Mỗi loại hình mới buộc phải SỬA getArea() (và mọi switch theo "kind" khác).
  • Sửa code đang chạy ổn = rủi ro gãy thứ không liên quan.
  • Hàm phình dần theo thời gian - càng lúc càng khó đọc, khó test.

Cho mỗi hình implement chung một interface; code dùng chỉ biết interface đó. Lệ thuộc vào abstraction thay vì lớp cụ thể như vậy chính là Dependency Inversion (DIP):

✅ Tuân thủ OCP - dùng interface

interface Shape {
  getArea(): number;
}

class Square implements Shape {
  constructor(private side: number) {}
  getArea() { return this.side * this.side; }
}

class Rectangle implements Shape {
  constructor(private w: number, private h: number) {}
  getArea() { return this.w * this.h; }
}

// Code dung chi biet Shape.getArea() - khong quan tam la hinh gi
function totalArea(shapes: Shape[]): number {
  return shapes.reduce((sum, s) => sum + s.getArea(), 0);
}
  • totalArea() đóng với sửa đổi: không cần đụng tới khi có hình mới.
  • Hệ thống mở với mở rộng: thêm hình = thêm một lớp implement Shape.
  • Tránh được "switch theo loại" rải rác khắp nơi.

✅ Mở rộng - chỉ THÊM, không SỬA

class Hexagon implements Shape {
  constructor(private side: number) {}
  getArea() { return (3 * Math.sqrt(3) / 2) * this.side ** 2; }
}

// Khong dong vao Square, Rectangle hay totalArea().
const shapes: Shape[] = [new Square(2), new Rectangle(2, 3), new Hexagon(1)];
totalArea(shapes); // hoat dong ngay

Abstract class cũng được

Khi các biến thể chia sẻ nhiều hành vi chung, bạn có thể dùng abstract class Shape với abstract getArea() và để lớp con override. Interface phù hợp khi chỉ cần "hợp đồng"; abstract class phù hợp khi có sẵn phần thân chung. Dù chọn cách nào, lớp con phải thay được lớp cha mà không phá hành vi - đó là Liskov Substitution (LSP).
  • Dấu hiệu vi phạm: switch/if rẽ theo type/kind cứ dài thêm mỗi lần có biến thể mới.
  • Điểm mở rộng nên đặt đúng "trục hay đổi" của bài toán - không mở mọi thứ.
  • OCP thường đi kèm SRP (mỗi biến thể một lớp) và là nền của pattern Strategy.

Đừng mở bừa

Trừu tượng hoá có giá: thêm lớp, thêm gián tiếp. Chỉ "mở" nơi bạn có lý do tin rằng sẽ còn thêm biến thể. Một bài toán chỉ có một biến thể và gần như không đổi thì if/switch đơn giản là đủ.

Câu hỏi thường gặp

Không tuyệt đối. Ý là: với những thay đổi ĐÃ LƯỜNG TRƯỚC (thêm một loại hình, một kiểu khuyến mãi…), bạn nên thêm code mới chứ đừng phải mổ lại code cũ đang chạy ổn. Sửa lỗi (bug fix) thì vẫn sửa bình thường. OCP nhắm vào hướng MỞ RỘNG, không phải cấm đụng file.

Nhìn vào trục hay thay đổi của bài toán. Nếu "sẽ còn thêm nhiều loại X", thì X là trục cần một điểm mở rộng (interface). Đừng mở mọi thứ - chỉ mở nơi bạn dự đoán hợp lý là sẽ biến đổi. Mở bừa = over-engineering.

Bài Áp dụng SOLID tổng hợp - cân bằng, đừng lạm dụng →

Một hai nhánh if đơn giản, ít đổi thì cứ để vậy (KISS). OCP đáng dùng khi chuỗi if/switch theo "loại" cứ phình ra mỗi lần thêm biến thể - đó là tín hiệu nên thay bằng đa hình (polymorphism) qua interface/abstract.

Cả hai đều được, nhưng hiện đại ưu tiên interface/đa hình (composition) hơn kế thừa sâu. Mỗi biến thể implement chung một interface; code dùng chỉ biết interface. Kế thừa abstract class cũng được khi các biến thể chia sẻ phần lớn hành vi.

Kế thừa an toàn: bài L - Liskov Substitution →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/5 Điểm: 0

Open/Closed Principle nghĩa là gì?

Bài tập về nhà

  1. 1

    Tìm chuỗi switch theo loại

    Tìm trong code một hàm có if/switch rẽ theo "loại" (type/kind/category) mà cứ thêm loại là phải sửa.

    ✅ Hoàn thành khi: Chỉ ra hàm cụ thể; nói rõ thêm một loại mới thì phải sửa những đâu.

  2. 2

    Mở bằng abstraction

    Refactor ví dụ tính diện tích (hoặc hàm em vừa tìm) sang interface/abstract để mỗi loại tự lo phần của nó.

    ✅ Hoàn thành khi: Thêm một loại mới CHỈ cần thêm một lớp, không sửa code cũ; biên dịch & test vẫn xanh.

  3. 3

    Thêm Hexagon

    Sau khi refactor, thêm hình lục giác (Hexagon) để chứng minh OCP: không đụng vào các lớp/hàm có sẵn.

    ✅ Hoàn thành khi: Chỉ thêm file/lớp mới; diff không chạm các lớp hình cũ.

  4. 4

    Khi nào KHÔNG cần OCP

    Nêu một ví dụ mà việc trừu tượng hoá là thừa (chỉ có một biến thể, gần như không đổi).

    ✅ Hoàn thành khi: Lập luận được rằng OCP đúng chỗ thì lợi, sai chỗ thì rối (over-engineering).