SOLID giúp code thay đổi với blast radius nhỏ hơn. Nó không phải mục tiêu tự thân; áp dụng máy móc có thể biến ba dòng code thành mười bảy interface đang chờ business requirement đầu tiên.
S — Single Responsibility
Một module/class nên có một lý do thay đổi mang tính stakeholder.
1 | class Invoice |
Tính tiền và render PDF thay đổi vì lý do khác nhau.
O — Open/Closed
Mở cho extension, đóng với modification không có nghĩa “không bao giờ sửa code cũ”. Nó khuyến khích stable abstraction:
1 | class Checkout |
Thêm gateway bằng object mới thay vì case provider lan khắp hệ thống.
L — Liskov Substitution
Subtype phải dùng thay base type mà không phá contract. Nếu subclass raise NotImplementedError cho method base hứa hỗ trợ, inheritance có thể sai abstraction.
Ruby duck typing không loại bỏ LSP; nó chỉ làm contract implicit hơn, nghĩa là test và naming phải làm việc chăm hơn.
I — Interface Segregation
Consumer không nên phụ thuộc method không dùng. Ruby module nhỏ hoặc protocol duck-typed giúp tách capability:
1 | module Refundable |
D — Dependency Inversion
Business policy phụ thuộc abstraction/capability, không phụ thuộc detail cụ thể:
1 | SendReceipt.new(mailer: RailsMailer.new) |
Inject dependency giúp test và thay implementation. Nhưng inject Time, logger, serializer, string formatter vào mọi object chỉ để “SOLID” có thể làm constructor trông như danh sách khách mời đám cưới.
Dùng SOLID khi nào?
- có nhiều implementation;
- dependency ngoài thay đổi/khó test;
- class có nhiều nhóm lý do thay đổi;
- conditional theo type/provider tăng dần;
- boundary domain cần giữ ổn định.
Không abstract trước khi thấy trục thay đổi. Duplication nhỏ đôi khi rẻ hơn abstraction sai.
Kết luận
SOLID là vocabulary để thảo luận coupling, contract và changeability. Thiết kế tốt không phải code có nhiều class nhất; nó là code khiến thay đổi dự đoán được.