EngineeringSenior DeveloperTraining Sessions

Session 06 - SOLID Principles: Thực Chiến, Không Giáo Điều

SOLID bị dạy quá giáo điều: "phải luôn tuân thủ". Thực tế, SOLID là công cụ tư duy, không phải luật. Senior/master biết khi nào áp dụng, khi nào vi phạm có chủ đích.


Mục tiêu

  • Hiểu bản chất từng principle (không phải thuộc lòng định nghĩa)
  • Biết khi nào SOLID giúp ích, khi nào nó over-engineering
  • Review code qua lăng kính SOLID thực chiến

S - Single Responsibility: "1 Lý Do Để Thay Đổi"

csharp
// ❌ Class có NHIỀU lý do thay đổi:
public class OrderService
{
    public Order CreateOrder(OrderRequest req) { /* business logic */ }
    public void SendConfirmationEmail(Order order) { /* email logic */ }
    public decimal CalculateDiscount(Order order) { /* pricing logic */ }
    public string GenerateInvoicePdf(Order order) { /* PDF logic */ }
}
// → Email thay đổi? Sửa OrderService.
// → Discount rules đổi? Sửa OrderService.
// → PDF format đổi? Sửa OrderService.
// → 1 class bị sửa vì 4 lý do khác nhau!

// ✅ Tách theo "lý do thay đổi":
public class OrderService       { /* Chỉ tạo/quản lý orders */ }
public class EmailService       { /* Chỉ gửi email */ }
public class PricingService     { /* Chỉ tính giá/discount */ }
public class InvoiceGenerator   { /* Chỉ tạo PDF */ }

Nhưng đừng quá đà:

csharp
// ❌ Over-SRP: tách quá nhỏ, class chỉ có 1 method
public class OrderValidator { void Validate(Order o) { } }
public class OrderSaver { void Save(Order o) { } }
public class OrderLogger { void Log(Order o) { } }
// → 20 tiny classes cho 1 feature → khó navigate, khó hiểu flow

// ✅ Thực chiến: tách theo BUSINESS CONCERN, không phải method
// OrderService xử lý multiple methods CÙNG concern (quản lý orders) = OK

O - Open/Closed: "Mở Rộng Không Cần Sửa Code Cũ"

csharp
// ❌ Thêm loại discount → phải sửa method cũ
public decimal CalculateDiscount(Order order)
{
    if (order.CustomerType == "VIP") return 0.2m;
    if (order.CustomerType == "Gold") return 0.1m;
    if (order.CustomerType == "New") return 0.05m;
    // Thêm loại mới → sửa if/else ở đây → vi phạm OCP
    return 0m;
}

// ✅ Strategy pattern: thêm loại mới = thêm class mới
public interface IDiscountStrategy
{
    bool CanApply(Order order);
    decimal Calculate(Order order);
}

public class VipDiscount : IDiscountStrategy { /* ... */ }
public class GoldDiscount : IDiscountStrategy { /* ... */ }
// Thêm StudentDiscount → tạo class mới, KHÔNG sửa code cũ

public class PricingService(IEnumerable<IDiscountStrategy> strategies)
{
    public decimal CalculateDiscount(Order order)
    {
        return strategies
            .Where(s => s.CanApply(order))
            .Max(s => s.Calculate(order));
    }
}

Nhưng đừng quá đà:

csharp
// ❌ Over-OCP: Strategy pattern cho 2 cases
// Nếu chỉ có 2 loại discount và KHÔNG CÓ KẼ HỞ sẽ thêm → if/else HOÀN TOÀN OK!
// Strategy pattern thêm complexity (interface, DI, multiple files) cho giá trị thấp

L - Liskov Substitution: "Subclass Thay Thế Được Base Class"

csharp
// ❌ Vi phạm LSP kinh điển:
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = value; base.Height = value; } // Force equal!
    }
    public override int Height
    {
        set { base.Height = value; base.Width = value; } // Force equal!
    }
}

// Code dùng Rectangle → bất ngờ khi nhận Square:
Rectangle r = new Square();
r.Width = 5;
r.Height = 3;
Console.WriteLine(r.Area); // Mong 15, nhận 9! 💀

// ✅ Fix: Composition, không inheritance sai
public interface IShape { int Area { get; } }
public record Rectangle(int Width, int Height) : IShape { public int Area => Width * Height; }
public record Square(int Side) : IShape { public int Area => Side * Side; }

Mindset thực chiến: Khi kế thừa, hỏi: "Caller dùng base class có bị surprise khi nhận subclass không?" Nếu có → đừng kế thừa.


I - Interface Segregation: "Interface Nhỏ, Hành Vi Rõ"

csharp
// ❌ Fat interface: caller bị buộc implement methods không cần
public interface IRepository<T>
{
    T GetById(int id);
    List<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
    List<T> Search(string query);
    void BulkInsert(List<T> entities);
    void ExecuteRawSql(string sql);
}
// → ReadOnlyService chỉ cần GetById và GetAll, 
//   nhưng phải implement Add, Update, Delete (throw NotImplementedException?)

// ✅ Tách interface theo USE CASE
public interface IReadRepository<T>
{
    T? GetById(int id);
    List<T> GetAll();
}

public interface IWriteRepository<T>
{
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
}

// Consumer chỉ inject cái họ CẦN
public class ProductListController(IReadRepository<Product> repo) { }
public class ProductAdminController(IReadRepository<Product> reader, IWriteRepository<Product> writer) { }

D - Dependency Inversion: "Depend Vào Abstraction"

csharp
// ❌ High-level module depend vào low-level detail
public class OrderService
{
    private readonly SqlOrderRepository _repo = new(); // Trực tiếp depend SQL!
    private readonly SmtpEmailService _email = new();   // Trực tiếp depend SMTP!
}
// → Đổi database? Sửa OrderService.
// → Test? Phải có SQL server thật!

// ✅ Depend vào abstraction (interface)
public class OrderService(IOrderRepository repo, IEmailService email)
{
    // KHÔNG biết implementation là SQL hay MongoDB
    // KHÔNG biết email là SMTP hay SendGrid
    // → Dễ test (mock), dễ thay đổi
}

Khi Nào SOLID Là Overkill?

✅ Dùng SOLID khi:
  - Code sẽ được NHIỀU NGƯỜI maintain
  - Yêu cầu THAY ĐỔI thường xuyên (business rules, integrations)
  - Cần TEST isolation

❌ SOLID quá mức khi:
  - Prototype / MVP (ship nhanh, refactor sau)
  - Script 1 lần (ETL, migration)
  - Simple CRUD (không có business logic phức tạp)
  - Team nhỏ, 1-2 người hiểu context

Quy tắc 3: Nếu pattern chỉ giúp khi có ≥3 variations → đợi đến lúc cần variation thứ 3 rồi refactor.


Master Checklist ✅

□ Class có >1 lý do thay đổi? → Tách (SRP)
□ Thêm feature = sửa if/else lớn? → Strategy/Plugin (OCP)
□ Subclass có gây surprise cho caller? → Đừng inherit (LSP)
□ Caller bị buộc implement methods không cần? → Tách interface (ISP)
□ Module depend vào concrete class? → Inject interface (DIP)
□ NHƯNG: quá nhiều interfaces cho CRUD đơn giản? → Simplify!

Tư duy master: SOLID là heuristics, không phải laws. Mỗi principle có "cost" (thêm abstraction, thêm files, thêm complexity). Chỉ trả "cost" đó khi benefit lớn hơn.