99 Ngày Java — Ngày 91: SOLID
SOLID là năm nguyên tắc giúp code hướng đối tượng dễ thay đổi: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation và Dependency Inversion. Chúng không phải luật phải tuân theo mọi lúc, mà là công cụ chẩn đoán: khi một thay đổi nhỏ buộc bạn sửa nhiều nơi, thường có một nguyên tắc đang bị vi phạm.

1. Single Responsibility: một lý do để thay đổi
Một lớp nên chỉ có một lý do để thay đổi, tức là phục vụ một nhóm người ra yêu cầu. Lớp vừa tính lương, vừa định dạng báo cáo, vừa ghi database sẽ bị sửa khi kế toán, bộ phận báo cáo hoặc DBA đổi ý, và mỗi lần sửa có thể làm hỏng hai phần còn lại. Tách theo lý do thay đổi, không tách theo số dòng: một lớp dài nhưng chỉ làm một việc vẫn ổn.
// before: three reasons to change in one class
class Invoice {
Money total() { ... }
String toPdf() { ... }
void save(Connection connection) { ... }
}
// after
record Invoice(List<Line> lines) { Money total() { ... } }
class InvoicePdfRenderer { byte[] render(Invoice invoice) { ... } }
class InvoiceRepository { void save(Invoice invoice) { ... } }2. Open/Closed và Liskov Substitution
Open/Closed: thêm hành vi mới bằng cách thêm code mới, không sửa code đang chạy tốt. Một chuỗi if theo loại phải sửa mỗi khi có loại mới là dấu hiệu rõ nhất; thay bằng interface và mỗi loại một lớp cài đặt. Liskov Substitution: ở đâu dùng được kiểu cha thì phải dùng được kiểu con mà không bất ngờ. Lớp con ném UnsupportedOperationException cho một phương thức của cha, hoặc siết chặt điều kiện đầu vào, là vi phạm.
interface ShippingFee { Money feeFor(Order order); }
class StandardShipping implements ShippingFee {
public Money feeFor(Order order) { return Money.of(30_000); }
}
class ExpressShipping implements ShippingFee {
public Money feeFor(Order order) { return Money.of(60_000); }
}
// A new shipping type is a new class; existing classes stay untouched.3. Interface Segregation và Dependency Inversion
Interface Segregation: client không nên bị buộc phụ thuộc vào phương thức nó không dùng. Một interface lớn làm mọi lớp cài đặt phải viết phương thức rỗng, và mọi client bị ảnh hưởng khi một phần không liên quan thay đổi. Dependency Inversion: logic nghiệp vụ phụ thuộc vào abstraction do chính nó định nghĩa, còn chi tiết như database hay cổng thanh toán cài đặt abstraction đó. Trong Java, điều này thường là constructor injection với một interface.
interface PaymentGateway { Receipt charge(String orderId, Money amount); }
class CheckoutService {
private final PaymentGateway gateway; // abstraction
CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
}
class StripeGateway implements PaymentGateway { ... } // detail4. Code smell và dùng SOLID có chừng mực
Vài tín hiệu đáng chú ý: lớp có tên chứa Manager hoặc Util và cứ lớn dần; một thay đổi nghiệp vụ nhỏ chạm vào nhiều file; chuỗi if hoặc switch theo loại lặp lại ở nhiều nơi; lớp con phải vô hiệu hóa hành vi của cha; test cần dựng cả database để kiểm một phép tính. Ngược lại, áp dụng SOLID trước khi có nhu cầu thật tạo ra tầng tầng interface chỉ có một cài đặt.
Chờ lần thay đổi thứ hai rồi mới trừu tượng hóa theo hướng thay đổi đó.
Ưu tiên composition hơn kế thừa; kế thừa chỉ khi quan hệ là-một thật sự đúng với hành vi.
Interface nhỏ đặt cạnh client dùng nó, không đặt cạnh lớp cài đặt.
Sealed type và switch đầy đủ là một cách khác để mở rộng an toàn khi tập loại do bạn kiểm soát.
SOLID trả lời một câu hỏi: khi yêu cầu đổi, phải sửa bao nhiêu chỗ. Ngày 92 bắt đầu loạt design pattern với hai mẫu tạo đối tượng: Singleton và Factory.
Tài liệu đối chiếu
https://en.wikipedia.org/wiki/SOLID
https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


