SOLID, Open-Closed và Dependency Inversion thực hành
Ôn lại năm nguyên lý SOLID qua các câu chuyện thực tế, đào sâu Open-Closed và Dependency Inversion với SimUDuck và Pizza Store.
Trung bìnhDesign pattern ~60 phút
Ôn lại trước khi học
Giới thiệu
Bài này ôn 5 nguyên lý SOLID qua ví dụ vịt và pizza. Bạn học cách thêm tính năng không sửa code cũ.
Kiến thức cần nhớ
S — Đơn trách nhiệm
- Một lớp chỉ có một lý do để đổi.
- Lớp ôm nhiều việc rất dễ hỏng dây chuyền.
- Ví dụ đời thường: một người chỉ làm một nghề.
- Tách lớp in hóa đơn ra khỏi lớp lưu món ăn.
O — Mở rộng không sửa đổi
- Thêm mới bằng cách thêm lớp mới.
- Không mở lớp cũ ra vá mỗi lần thêm.
- Ví dụ đời thường: cắm thêm ổ điện, không đục tường.
- Vịt bay tên lửa là lớp mới, lớp Vịt giữ nguyên.
L, I, D còn lại
- L: vịt con phải dùng được như vịt cha.
- I: đừng ép lớp dùng thứ nó không cần.
- D: phụ thuộc vào giao diện, không phụ thuộc lớp cụ thể.
- Ví dụ đời thường: remote chỉ cần nút bấm chung.
| Chữ | Câu hỏi tự vấn |
|---|---|
| S | Lớp này có mấy lý do để đổi? |
| O | Thêm mới có phải sửa cũ không? |
| L | Lớp con có gây bất ngờ không? |
| D | Đang phụ thuộc vào cụ thể hay trừu tượng? |
Ví dụ
- Input: thêm kiểu bay tên lửa cho đàn vịt. Output: tạo lớp mới, lớp Vịt nguyên vẹn.
- Giải thích: đúng chữ O vì mở rộng mà không sửa cũ.
💻 Bài tập
Ngôn ngữ: C++17 (hoặc Java). Nộp vào day-003/. Mỗi bài có thư mục before/ và after/.
Bài 1: Tách trách nhiệm (S)
Cho class sau, viết lại đúng như đề trong before/order.cpp:
class Order {
public:
void addItem(std::string name, double price, int qty);
double total() const; // tính tiền
std::string toInvoiceText() const; // định dạng hóa đơn
void saveToFile(const std::string &p); // ghi file
void sendEmail(const std::string &to); // gửi email (chỉ in ra console)
};
Yêu cầu: tách thành 4 lớp, mỗi lớp một lý do để đổi.
| Lớp | Trách nhiệm |
|---|---|
Order | Giữ danh sách món, tính total() |
InvoiceFormatter | std::string format(const Order&) |
OrderRepository | void save(const Order&, const std::string &path) |
EmailSender | void send(const std::string &to, const std::string &body) |
Trong README.md, với mỗi lớp ghi 1 ví dụ thay đổi yêu cầu chỉ làm đổi lớp đó.
Bài 2: Mở rộng không sửa (O) và đảo phụ thuộc (D)
Code ban đầu tính giảm giá bằng chuỗi if:
double discount(const std::string &type, double total) {
if (type == "none") return 0;
if (type == "percent10") return total * 0.1;
if (type == "fixed50") return total > 50 ? 50 : 0;
return 0;
}
Yêu cầu:
- Tạo interface
class DiscountPolicy { public: virtual double apply(double total) const = 0; virtual ~DiscountPolicy() = default; }; - Viết 3 lớp:
NoDiscount,PercentDiscount(double rate),FixedDiscount(double amount, double minTotal). Checkoutnhậnstd::unique_ptr<DiscountPolicy>qua constructor.Checkoutkhông được#includelớp cụ thể nào.- Thêm
BuyOver200Get15Percentmà không sửa dòng nào củaCheckoutvà 3 lớp cũ.
total=100, NoDiscount -> 100
total=100, PercentDiscount(0.1) -> 90
total=40, FixedDiscount(50, 50) -> 40
total=120, FixedDiscount(50, 50) -> 70
total=250, BuyOver200Get15Percent -> 212.5
Bài 3: Phát hiện vi phạm L và I (nâng cao)
- Viết
RectanglecósetWidth,setHeight,area. ChoSquarekế thừaRectangle. Viết hàm testvoid resize(Rectangle &r) { r.setWidth(5); r.setHeight(4); assert(r.area() == 20); }. Chứng minhSquarelàm test fail. - Sửa thiết kế để test không còn fail. Ví dụ: tách interface
Shape { virtual double area() const = 0; }. - Cho interface
IMachine { print(); scan(); fax(); }. Máy in rẻ chỉ in được. Tách thànhIPrinter,IScanner,IFaxđể máy in rẻ không phải cài hàm rỗng.
✅ Tiêu chí hoàn thành
- Mỗi bài có
before/vàafter/, cả hai build và chạy được. - Bài 2:
git diffkhi thêm policy mới chỉ có file mới, không sửa file cũ. - Bài 3: có test cho thấy
Squarephá LSP, và bản sửa pass. -
README.mdtrả lời câu tự vấn trong bảng SOLID cho từng bài.
❓ Câu hỏi ôn tập
- S nghĩa là gì, cho ví dụ?
- O nghĩa là gì, cho ví dụ?
- D nghĩa là gì, cho ví dụ?
- Dấu hiệu nào cho thấy code vi phạm SOLID?
🌕 Bạn đã hoàn thành ngày 3. Làm hết bài tập phía trên rồi sang ngày tiếp theo.