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

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
SLớp này có mấy lý do để đổi?
OThêm mới có phải sửa cũ không?
LLớ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ớpTrách nhiệm
OrderGiữ danh sách món, tính total()
InvoiceFormatterstd::string format(const Order&)
OrderRepositoryvoid save(const Order&, const std::string &path)
EmailSendervoid 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:

  1. Tạo interface class DiscountPolicy { public: virtual double apply(double total) const = 0; virtual ~DiscountPolicy() = default; };
  2. Viết 3 lớp: NoDiscount, PercentDiscount(double rate), FixedDiscount(double amount, double minTotal).
  3. Checkout nhận std::unique_ptr<DiscountPolicy> qua constructor. Checkout không được #include lớp cụ thể nào.
  4. Thêm BuyOver200Get15Percent mà không sửa dòng nào của Checkout và 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)

  1. Viết Rectangle có setWidth, setHeight, area. Cho Square kế thừa Rectangle. Viết hàm test void resize(Rectangle &r) { r.setWidth(5); r.setHeight(4); assert(r.area() == 20); }. Chứng minh Square làm test fail.
  2. Sửa thiết kế để test không còn fail. Ví dụ: tách interface Shape { virtual double area() const = 0; }.
  3. Cho interface IMachine { print(); scan(); fax(); }. Máy in rẻ chỉ in được. Tách thành IPrinter, 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 diff khi thêm policy mới chỉ có file mới, không sửa file cũ.
  • Bài 3: có test cho thấy Square phá LSP, và bản sửa pass.
  • README.md trả 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.

Bớt paste mù, hiểu sâu từng dòng mình viết.

180 Days Challenge. 180 ngày cai nghiện vibe code.