SOLID — 5 nguyên lý thiết kế
SOLID là gì?
SOLID = 5 nguyên lý OOP của Robert C. Martin (Uncle Bob):
S — Single Responsibility Principle
A class should have one, and only one, reason to change.
❌ Vi phạm SRP — Class ôm nhiều việc
public class Order {
public int Id { get; set; }
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; }
public decimal CalculateTotal() { // tính toán
return Items.Sum(i => i.Price * i.Quantity);
}
public void SaveToDatabase() { // persistence
using var conn = new SqlConnection(...);
// INSERT INTO Orders ...
}
public string FormatForInvoice() { // formatting
return $"Order #{Id}: ${Total}\n" +
string.Join("\n", Items.Select(i => $"- {i.Name}"));
}
public void SendEmail(string to) { // communication
new SmtpClient().Send(to, "Your order", FormatForInvoice());
}
}
3 lý do thay đổi: đổi DB → sửa class. Đổi format invoice → sửa class. Đổi cách gửi mail → sửa class. Vi phạm SRP.
✅ Tuân thủ SRP — tách thành nhóm có trách nhiệm rõ
// Entity — chỉ chứa state
public class Order {
public int Id { get; set; }
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
// Business logic — tính toán
public class OrderCalculator {
public decimal CalculateTotal(Order order) =>
order.Items.Sum(i => i.Price * i.Quantity);
}
// Persistence — lưu trữ
public class OrderRepository {
private readonly DbContext _ctx;
public OrderRepository(DbContext ctx) => _ctx = ctx;
public async Task SaveAsync(Order order) { /* ... */ }
}
// Formatting
public class OrderInvoiceFormatter {
public string Format(Order order) =>
$"Order #{order.Id}: ${order.Total}\n" +
string.Join("\n", order.Items.Select(i => $"- {i.Name}"));
}
// Communication
public class OrderEmailService {
public async Task SendInvoiceAsync(Order order, string to) { /* ... */ }
}
O — Open/Closed Principle
Open for extension, closed for modification.
Thêm tính năng mới = thêm code mới, không sửa code cũ.
❌ Vi phạm OCP
public class AreaCalculator {
public double Calculate(object shape) {
if (shape is Rectangle r) return r.Width * r.Height;
if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
// Mỗi khi thêm shape — phải sửa method này
throw new ArgumentException();
}
}
Thêm Triangle → mở file → thêm if → có thể vỡ test cho Rectangle/Circle. Fragile.
✅ Tuân thủ OCP — Polymorphism
public abstract class Shape {
public abstract double Area();
}
public class Rectangle : Shape {
public double Width, Height;
public override double Area() => Width * Height;
}
public class Circle : Shape {
public double Radius;
public override double Area() => Math.PI * Radius * Radius;
}
public class Triangle : Shape { // ⭐ Thêm class, KHÔNG động code cũ
public double Base, Height;
public override double Area() => 0.5 * Base * Height;
}
public class AreaCalculator {
public double Total(IEnumerable<Shape> shapes) =>
shapes.Sum(s => s.Area());
}
L — Liskov Substitution Principle
If S is a subtype of T, then objects of T may be replaced with objects of S without altering desirable properties.
Nói cách khác: subclass dùng được mọi nơi base class dùng được, không gây bất ngờ.
❌ Vi phạm LSP kinh điển — Penguin
public class Bird {
public virtual void Fly() => Console.WriteLine("Flying");
}
public class Sparrow : Bird {}
public class Penguin : Bird {
public override void Fly() {
throw new NotSupportedException("Penguins can't fly!");
}
}
void MakeBirdsFly(List<Bird> birds) {
foreach (var b in birds) b.Fly(); // Penguin trong list → CRASH
}
Caller có signature List<Bird> → kỳ vọng mọi Bird Fly() được. Penguin throw → phá expectation. Vi phạm LSP.
✅ Fix — Tách hierarchy
public abstract class Bird {
public abstract string Eat();
}
public interface IFlyable {
void Fly();
}
public class Sparrow : Bird, IFlyable {
public override string Eat() => "Seeds";
public void Fly() => Console.WriteLine("Sparrow flying");
}
public class Penguin : Bird { // không implement IFlyable
public override string Eat() => "Fish";
}
// Caller nhận IFlyable — không có Penguin → không crash
void MakeFly(List<IFlyable> birds) { ... }
Vi phạm LSP tinh tế hơn — Square : Rectangle
public class Rectangle {
public virtual int Width { get; set; }
public virtual int Height { get; set; }
}
public class Square : Rectangle {
// Square = Rect đặc biệt — width = height
public override int Width {
set { base.Width = value; base.Height = value; }
}
public override int Height {
set { base.Width = value; base.Height = value; }
}
}
void TestArea(Rectangle r) {
r.Width = 5;
r.Height = 4;
Debug.Assert(r.Width * r.Height == 20); // FAIL với Square (= 16)
}
Caller có Rectangle → set width, height độc lập, tính area = w*h. Square phá assumption. Vi phạm LSP.
Fix: Square và Rectangle không nên kế thừa nhau — cả 2 có thể inherit Shape, hoặc immutable + factory.
I — Interface Segregation Principle
Clients should not be forced to depend on methods they do not use.
Nhiều interface nhỏ, chuyên biệt > 1 interface to.
❌ Vi phạm — Fat interface
public interface IWorker {
void Work();
void Eat();
void Sleep();
void Pay();
}
// Robot phải implement Eat, Sleep — không hợp logic
public class Robot : IWorker {
public void Work() { /* OK */ }
public void Eat() => throw new NotSupportedException();
public void Sleep() => throw new NotSupportedException();
public void Pay() { /* OK */ }
}
✅ Fix — Tách nhỏ
public interface IWorkable { void Work(); }
public interface IFeedable { void Eat(); }
public interface IRestable { void Sleep(); }
public interface IPayable { void Pay(); }
public class Human : IWorkable, IFeedable, IRestable, IPayable { ... }
public class Robot : IWorkable, IPayable { ... } // chỉ implement cần thiết
Trong TypeScript — interface composition
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Restable { sleep(): void; }
class Human implements Workable, Feedable, Restable {
work() {} eat() {} sleep() {}
}
class Robot implements Workable {
work() {}
}
D — Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Vi phạm DIP
public class EmailService {
public void Send(string to, string subject) { /* SMTP code */ }
}
public class OrderService {
private readonly EmailService _email = new(); // gắn cứng concrete
public void PlaceOrder(Order order) {
// ...
_email.Send(order.UserEmail, "Order placed");
}
}
OrderService không thể test — chạy là gửi email thật. Đổi sang SMS = sửa OrderService.
✅ Fix — Depend abstraction
public interface INotificationService {
Task SendAsync(string to, string subject, string body);
}
public class EmailNotificationService : INotificationService {
public async Task SendAsync(string to, string subject, string body) { /* SMTP */ }
}
public class SmsNotificationService : INotificationService {
public async Task SendAsync(string to, string subject, string body) { /* Twilio */ }
}
public class OrderService {
private readonly INotificationService _notifier;
// Depend abstraction — inject via constructor
public OrderService(INotificationService notifier) => _notifier = notifier;
public async Task PlaceOrderAsync(Order order) {
// ...
await _notifier.SendAsync(order.UserEmail, "Order placed", "...");
}
}
DIP vs DI vs IoC
- DIP: nguyên lý "depend abstraction".
- IoC (Inversion of Control): nguyên lý lớn hơn — "đảo control flow", framework gọi code bạn (Hollywood principle).
- DI (Dependency Injection): kỹ thuật thực thi DIP/IoC — constructor / setter / framework container.
Bonus — SOLID khi nào không áp dụng
- Script nhỏ 100 dòng — đừng tách 5 class.
- Prototype, throwaway code — code nhanh > clean.
- "1 implementation, 1 interface" cho mọi class → noise — chỉ tạo interface khi có ≥ 2 impl thật hoặc cần test mock.