OCP nói module nên mở để mở rộng, đóng để sửa đổi: thêm hành vi mới bằng cách thêm code, không phải bằng cách sửa code đang chạy. Nhưng OCP không nói hãy dựng sẵn điểm mở rộng cho mọi thứ — chỉ chỗ nào bạn đã biết sẽ thay đổi.
Cách áp dụng có kiểm soát: chờ đến lần thứ ba.
1. Lần đầu viết thẳng.
2. Lần thứ hai xuất hiện biến thể, sao chép hoặc thêm nhánh — vẫn chưa trừu tượng hóa, vì hai điểm dữ liệu chưa đủ để biết trục thay đổi thật.
3. Lần thứ ba, hình dạng của điểm thay đổi đã rõ → tách abstraction đúng chỗ đó.
Lý do phải chờ: abstraction sai còn tốn kém hơn trùng lặp. Trùng lặp thì sửa nhiều chỗ nhưng nhìn thấy được; abstraction sai kéo mọi trường hợp mới phải bẻ cong cho vừa, và tham số cứ mọc thêm mỗi lần có yêu cầu mới.
Dạng mở rộng thường dùng và rẻ:
// registry: thêm loại mới = thêm entry, không sửa dispatcher
const handlers: Record<EventType, Handler> = {}
export function register(type: EventType, h: Handler) { handlers[type] = h }Dấu hiệu bạn đang mở rộng sai chỗ: interface chỉ có một implementation qua nhiều tháng; tham số options phình ra vì mỗi tính năng mới thêm một cờ; hoặc muốn thêm hành vi thì vẫn phải sửa cả class gốc lẫn phần mở rộng — nghĩa là điểm mở rộng đặt sai vị trí.