5.2 — 1. SOLID Principles
Mục tiêu bài học
- Nắm được ý chính của bài và mối liên hệ với module.
- Áp dụng được kiến thức vào bối cảnh CRM/.NET backend.
- Sẵn sàng chuyển sang bài kế tiếp với nền tảng chắc chắn.
Nội dung bài học
SOLID là 5 nguyên lý thiết kế giúp code dễ mở rộng, dễ test, ít bug khi thay đổi yêu cầu.
5.2.1 — 1.1 SRP — Single Responsibility Principle
Mỗi class chỉ có một lý do để thay đổi.
Vi phạm — God class CustomerService:
// BAD: một class làm quá nhiều việc
public class CustomerService
{
private readonly AppDbContext _db;
private readonly IEmailService _email;
public async Task<Customer> CreateAsync(CreateCustomerDto dto)
{
// 1. Validate
if (string.IsNullOrWhiteSpace(dto.Email))
throw new ArgumentException("Email is required");
// 2. Lưu database
var customer = new Customer { Name = dto.Name, Email = dto.Email };
_db.Customers.Add(customer);
await _db.SaveChangesAsync();
// 3. Gửi email chào mừng
await _email.SendAsync(dto.Email, "Chào mừng!", $"Xin chào {dto.Name}");
// 4. Tạo log audit
_db.AuditLogs.Add(new AuditLog { Action = "CreateCustomer", UserId = dto.CreatedBy });
await _db.SaveChangesAsync();
// 5. Tính điểm loyalty
customer.LoyaltyPoints = CalculateLoyaltyPoints(dto);
await _db.SaveChangesAsync();
return customer;
}
private int CalculateLoyaltyPoints(CreateCustomerDto dto) => 100;
}
Đúng — Tách trách nhiệm:
// Chỉ lo business logic của Customer
public class CustomerService
{
private readonly ICustomerRepository _repo;
private readonly ICustomerEventPublisher _events;
public async Task<Customer> CreateAsync(CreateCustomerDto dto)
{
var customer = Customer.Create(dto.Name, dto.Email); // domain factory
await _repo.AddAsync(customer);
await _events.PublishAsync(new CustomerCreatedEvent(customer)); // fire-and-forget
return customer;
}
}
// Chỉ lo gửi email
public class WelcomeEmailHandler : IEventHandler<CustomerCreatedEvent>
{
public async Task HandleAsync(CustomerCreatedEvent e)
=> await _email.SendAsync(e.Customer.Email, "Chào mừng!", $"Xin chào {e.Customer.Name}");
}
// Chỉ lo audit
public class AuditHandler : IEventHandler<CustomerCreatedEvent>
{
public async Task HandleAsync(CustomerCreatedEvent e)
=> await _audit.LogAsync("CreateCustomer", e.Customer.Id);
}
5.2.2 — 1.2 OCP — Open/Closed Principle
Mở để mở rộng, đóng để sửa đổi.
Vi phạm — Thêm discount type phải sửa code cũ:
// BAD: mỗi loại discount mới buộc phải sửa switch
public decimal CalculateDiscount(Order order, string discountType)
{
return discountType switch
{
"VIP" => order.Total * 0.20m,
"Seasonal" => order.Total * 0.10m,
// Thêm "NewYear" → phải sửa file này → vi phạm OCP
_ => 0
};
}
Đúng — Dùng abstraction:
public interface IDiscountStrategy
{
bool IsApplicable(Order order);
decimal Calculate(Order order);
}
public class VipDiscount : IDiscountStrategy
{
public bool IsApplicable(Order order) => order.Customer.IsVip;
public decimal Calculate(Order order) => order.Total * 0.20m;
}
public class SeasonalDiscount : IDiscountStrategy
{
public bool IsApplicable(Order order) => DateTime.Now.Month == 12;
public decimal Calculate(Order order) => order.Total * 0.10m;
}
// Thêm NewYearDiscount → chỉ tạo class mới, không chạm code cũ
public class NewYearDiscount : IDiscountStrategy
{
public bool IsApplicable(Order order) => DateTime.Now.DayOfYear <= 7;
public decimal Calculate(Order order) => order.Total * 0.15m;
}
public class DiscountCalculator
{
private readonly IEnumerable<IDiscountStrategy> _strategies;
public decimal Calculate(Order order)
=> _strategies
.Where(s => s.IsApplicable(order))
.Sum(s => s.Calculate(order));
}
5.2.3 — 1.3 LSP — Liskov Substitution Principle
Subtype phải thay thế được base type mà không phá vỡ behavior.
Vi phạm — Square extends Rectangle gây bug:
// BAD: Square override setter của Rectangle gây hành vi bất ngờ
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 = base.Height = value; } }
public override int Height { set { base.Width = base.Height = value; } }
}
// Code này đúng với Rectangle nhưng fail với Square
Rectangle r = new Square();
r.Width = 5;
r.Height = 3;
Console.WriteLine(r.Area()); // mong đợi 15, thực tế ra 9 → LSP vi phạm
Đúng — Dùng composition hoặc tách interface:
public interface IShape { int Area(); }
public class Rectangle : IShape
{
public int Width { get; init; }
public int Height { get; init; }
public int Area() => Width * Height;
}
public class Square : IShape
{
public int Side { get; init; }
public int Area() => Side * Side;
}
5.2.4 — 1.4 ISP — Interface Segregation Principle
Client không nên bị buộc phụ thuộc vào method mà nó không dùng.
Vi phạm — Interface quá fat:
// BAD: mọi service đều phải implement dù không dùng hết
public interface ICustomerRepository
{
Task<Customer> GetByIdAsync(int id);
Task AddAsync(Customer c);
Task UpdateAsync(Customer c);
Task DeleteAsync(int id);
Task<List<Customer>> SearchAsync(string keyword);
Task<byte[]> ExportToCsvAsync(); // ← report service không cần cái này
Task SendBulkEmailAsync(string msg); // ← hoàn toàn không liên quan repository
}
Đúng — Tách interface nhỏ:
public interface ICustomerReader
{
Task<Customer?> GetByIdAsync(int id);
Task<List<Customer>> SearchAsync(string keyword);
}
public interface ICustomerWriter
{
Task AddAsync(Customer c);
Task UpdateAsync(Customer c);
Task DeleteAsync(int id);
}
public interface ICustomerExporter
{
Task<byte[]> ExportToCsvAsync();
}
// CRM report service chỉ cần ICustomerReader + ICustomerExporter
5.2.5 — 1.5 DIP — Dependency Inversion Principle
Module cấp cao không phụ thuộc module cấp thấp; cả hai phụ thuộc abstraction.
Vi phạm — Service phụ thuộc concrete:
// BAD: LeadService tự new() SqlCustomerRepository → khó test, khó đổi DB
public class LeadService
{
private readonly SqlCustomerRepository _repo = new SqlCustomerRepository();
public async Task<Lead> ConvertAsync(int leadId)
{
var lead = await _repo.GetLeadByIdAsync(leadId);
// ...
}
}
Đúng — Inject abstraction:
public class LeadService
{
private readonly ILeadRepository _repo;
private readonly ICustomerRepository _customerRepo;
// Constructor injection — DI container cung cấp implementation
public LeadService(ILeadRepository repo, ICustomerRepository customerRepo)
{
_repo = repo;
_customerRepo = customerRepo;
}
public async Task<Customer> ConvertAsync(int leadId)
{
var lead = await _repo.GetByIdAsync(leadId)
?? throw new NotFoundException($"Lead {leadId} not found");
var customer = Customer.FromLead(lead);
await _customerRepo.AddAsync(customer);
lead.MarkConverted(customer.Id);
await _repo.UpdateAsync(lead);
return customer;
}
}
Bài tập áp dụng
- Tóm tắt bài học bằng ngôn ngữ của bạn.
- Liên hệ nội dung với một tình huống thực tế trong dự án.
- Đề xuất một cải tiến cụ thể sau khi học bài này.
Tự kiểm tra
- Bạn có thể giải thích lại nội dung chính trong 2 phút không?
- Bạn có ví dụ áp dụng thực tế chưa?
- Bạn biết bước tiếp theo cần học/triển khai là gì không?
Kết luận
Hoàn thành bài này giúp bạn có góc nhìn đầy đủ hơn trước khi đi tiếp trong module.
Điều hướng
- Bài trước: 5.1 — Module Orientation
- Bài tiếp theo: 5.3 — 2. Generics
- Về module: Trang mục lục