OOP

SOLID — 5 nguyên lý thiết kế

SRP, OCP, LSP, ISP, DIP — case study refactor mỗi nguyên lý. Ví dụ C# + TypeScript.

SOLID là gì?

SOLID = 5 nguyên lý OOP của Robert C. Martin (Uncle Bob):

S
Single Responsibility
Một class chỉ một lý do để thay đổi.
O
Open/Closed
Mở để mở rộng, đóng để sửa đổi.
L
Liskov Substitution
Subclass dùng được thay cho base class.
I
Interface Segregation
Nhiều interface nhỏ tốt hơn một interface to.
D
Dependency Inversion
Depend abstraction, không depend concrete.
PV gần như chắc chắn hỏi SOLID. Học hiểu — đừng học thuộc.

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) { /* ... */ }
}
"Reason to change" là gì? Robert C. Martin sau này refine: "should have one reason to change" = "should be responsible to one actor" (1 nhóm người dùng/stakeholder). Tính toán = team accounting; Format = team marketing; Save = team infra. Đổi cho 1 actor không nên ảnh hưởng actor khác.

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());
}
OCP thực dụng: không cần "đóng" mọi thứ. Đóng những điểm có khả năng thay đổi cao (thêm shape, thêm payment method, thêm export format). Còn lại sửa cũng không sao.

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.

LSP là lý do mạnh nhất cho "Favor composition over inheritance" — composition không có ràng buộc behavior này.

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
⭐ ISP là liên hệ với LSP — nếu interface fat → subtype có method không dùng được → throw → vi phạm LSP.

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

Câu PV cực hay:
  • 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.
Trong ASP.NET Core, NestJS, Spring → DI container tự build dependency graph.

Bonus — SOLID khi nào không áp dụng

SOLID là guideline, không phải luật. Lạm dụng → over-engineering:
  • 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.
Quy tắc thực dụng: áp dụng khi code grow và cảm thấy đau. Đừng áp dụng "preemptive".
Tiếp theo: Design Pattern

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.