Back to blog

Nguyên tắc SOLID: Viết Code Sạch, Dễ Bảo trì

oopsolid-principlesdesign-patternstypescriptpythonjava
Nguyên tắc SOLID: Viết Code Sạch, Dễ Bảo trì

Giới thiệu

Bạn đã học các nền tảng của OOP—classes, encapsulation, inheritance và polymorphism. Nhưng biết cú pháp OOP thôi chưa đủ. Làm sao quyết định khi nào tạo class mới? Các class nên quan hệ với nhau như thế nào? Điều gì làm code dễ thay đổi sau vài tháng?

Các nguyên tắc SOLID trả lời những câu hỏi này. Được đặt tên bởi Robert C. Martin (Uncle Bob), năm nguyên tắc này là cầu nối giữa việc hiểu OOP và viết phần mềm thực sự dễ bảo trì, kiểm thử và mở rộng.

Bạn Sẽ Học Được

✅ Áp dụng Single Responsibility Principle để tạo class tập trung
✅ Dùng Open/Closed Principle để mở rộng hành vi mà không sửa code
✅ Tôn trọng Liskov Substitution Principle cho kế thừa an toàn
✅ Thiết kế với Interface Segregation Principle cho contracts gọn gàng
✅ Triển khai Dependency Inversion Principle cho kiến trúc linh hoạt
✅ Nhận diện vi phạm SOLID và refactor từng bước

Điều kiện tiên quyết


Tại sao SOLID Quan trọng

Trước khi đi vào từng nguyên tắc, hãy hiểu các vấn đề mà SOLID giải quyết:

Vấn đềKhông có SOLIDCó SOLID
Thêm tính năngSửa class hiện có, rủi ro phá vỡ codeThêm class mới, code hiện có không đổi
TestingKhông thể test một thứ mà không setup mọi thứMỗi class test được độc lập
Hiểu codeMột class làm 10 việcMỗi class có mục đích rõ ràng
Thay đổi yêu cầuThay đổi lan truyền khắp codebaseThay đổi cô lập trong module cụ thể
Làm việc nhómMọi người sửa cùng fileCác nhóm làm trên module riêng biệt

SOLID không phải tuân theo quy tắc một cách mù quáng—mà là quản lý độ phức tạp khi codebase phát triển.


S — Single Responsibility Principle (SRP)

Một class chỉ nên có một lý do để thay đổi.

SRP nói rằng mỗi class nên làm một việc và làm tốt. Nếu class có nhiều trách nhiệm, thay đổi một trách nhiệm có thể phá vỡ trách nhiệm khác.

Vi phạm: God Class

// TypeScript - Vi phạm SRP
class UserService {
  // Trách nhiệm 1: Quản lý dữ liệu user
  createUser(name: string, email: string): User {
    const user = { id: Date.now(), name, email };
    this.saveToDatabase(user);
    this.sendWelcomeEmail(user);
    this.logAction(`User created: ${email}`);
    return user;
  }
 
  // Trách nhiệm 2: Thao tác database
  private saveToDatabase(user: User): void {
    console.log(`INSERT INTO users VALUES (${user.id}, '${user.name}', '${user.email}')`);
  }
 
  // Trách nhiệm 3: Gửi email
  private sendWelcomeEmail(user: User): void {
    console.log(`Sending welcome email to ${user.email}`);
  }
 
  // Trách nhiệm 4: Logging
  private logAction(message: string): void {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${message}`);
  }
}

Tại sao không tốt? Nếu bạn đổi email provider, bạn phải sửa UserService. Đổi database cũng sửa UserService. Đổi format logging cũng sửa UserService. Một class, bốn lý do để thay đổi.

Refactor: Mỗi Class, Một Công việc

// TypeScript - Áp dụng SRP
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
// Trách nhiệm: Lưu trữ users
class UserRepository {
  save(user: User): void {
    console.log(`Saving user ${user.email} to database`);
  }
 
  findByEmail(email: string): User | null {
    console.log(`Finding user by email: ${email}`);
    return null;
  }
}
 
// Trách nhiệm: Gửi email
class EmailService {
  sendWelcome(user: User): void {
    console.log(`Sending welcome email to ${user.email}`);
  }
 
  sendPasswordReset(user: User): void {
    console.log(`Sending password reset to ${user.email}`);
  }
}
 
// Trách nhiệm: Ghi log
class ActionLogger {
  log(message: string): void {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${message}`);
  }
}
 
// Trách nhiệm: Điều phối tạo user
class UserService {
  constructor(
    private repository: UserRepository,
    private emailService: EmailService,
    private logger: ActionLogger
  ) {}
 
  createUser(name: string, email: string): User {
    const user: User = { id: Date.now(), name, email };
    this.repository.save(user);
    this.emailService.sendWelcome(user);
    this.logger.log(`User created: ${email}`);
    return user;
  }
}

Giờ UserService chỉ có một việc: điều phối tạo user. Mỗi cộng tác viên xử lý trách nhiệm riêng.

# Python - Áp dụng SRP
from dataclasses import dataclass
from datetime import datetime
 
@dataclass
class User:
    id: int
    name: str
    email: str
 
class UserRepository:
    """Trách nhiệm: Lưu trữ users"""
    def save(self, user: User) -> None:
        print(f"Saving user {user.email} to database")
 
    def find_by_email(self, email: str) -> User | None:
        print(f"Finding user by email: {email}")
        return None
 
class EmailService:
    """Trách nhiệm: Gửi email"""
    def send_welcome(self, user: User) -> None:
        print(f"Sending welcome email to {user.email}")
 
class ActionLogger:
    """Trách nhiệm: Ghi log"""
    def log(self, message: str) -> None:
        timestamp = datetime.now().isoformat()
        print(f"[{timestamp}] {message}")
 
class UserService:
    """Trách nhiệm: Điều phối tạo user"""
    def __init__(
        self,
        repository: UserRepository,
        email_service: EmailService,
        logger: ActionLogger
    ):
        self._repository = repository
        self._email_service = email_service
        self._logger = logger
 
    def create_user(self, name: str, email: str) -> User:
        user = User(id=int(datetime.now().timestamp()), name=name, email=email)
        self._repository.save(user)
        self._email_service.send_welcome(user)
        self._logger.log(f"User created: {email}")
        return user
// Java - Áp dụng SRP
public class UserRepository {
    // Trách nhiệm: Lưu trữ users
    public void save(User user) {
        System.out.println("Saving user " + user.getEmail() + " to database");
    }
 
    public Optional<User> findByEmail(String email) {
        System.out.println("Finding user by email: " + email);
        return Optional.empty();
    }
}
 
public class EmailService {
    // Trách nhiệm: Gửi email
    public void sendWelcome(User user) {
        System.out.println("Sending welcome email to " + user.getEmail());
    }
}
 
public class ActionLogger {
    // Trách nhiệm: Ghi log
    public void log(String message) {
        String timestamp = java.time.Instant.now().toString();
        System.out.println("[" + timestamp + "] " + message);
    }
}
 
public class UserService {
    // Trách nhiệm: Điều phối tạo user
    private final UserRepository repository;
    private final EmailService emailService;
    private final ActionLogger logger;
 
    public UserService(UserRepository repository, EmailService emailService, ActionLogger logger) {
        this.repository = repository;
        this.emailService = emailService;
        this.logger = logger;
    }
 
    public User createUser(String name, String email) {
        User user = new User(System.currentTimeMillis(), name, email);
        repository.save(user);
        emailService.sendWelcome(user);
        logger.log("User created: " + email);
        return user;
    }
}

Cách Nhận biết Vi phạm SRP

Hãy tự hỏi:

  1. Bạn có thể mô tả class với "và"? — "Class này quản lý users và gửi email và ghi log" = vi phạm
  2. Thay đổi không liên quan có buộc phải sửa không? — Đổi email provider không nên chạm vào logic user
  3. Class có khó đặt tên không? — Nếu cần tên chung chung như Manager hay Handler, có lẽ nó làm quá nhiều việc

O — Open/Closed Principle (OCP)

Các thực thể phần mềm nên mở cho mở rộng, nhưng đóng cho sửa đổi.

Bạn nên có thể thêm hành vi mới mà không thay đổi code hiện có. Điều này đạt được thông qua abstraction và polymorphism.

Vi phạm: Switch Statements Phình ra

// TypeScript - Vi phạm OCP
class DiscountCalculator {
  calculate(customerType: string, amount: number): number {
    switch (customerType) {
      case "regular":
        return amount * 0.05;
      case "premium":
        return amount * 0.10;
      case "vip":
        return amount * 0.20;
      // Mỗi loại khách hàng mới đều phải sửa class này!
      default:
        return 0;
    }
  }
}

Tại sao không tốt? Thêm loại khách hàng mới (ví dụ: "enterprise") yêu cầu sửa đổi DiscountCalculator. Mỗi lần sửa đều rủi ro phá vỡ logic giảm giá hiện có.

Refactor: Mở cho Mở rộng

// TypeScript - Áp dụng OCP
 
interface DiscountStrategy {
  calculate(amount: number): number;
}
 
class RegularDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.05;
  }
}
 
class PremiumDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.10;
  }
}
 
class VIPDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.20;
  }
}
 
// Thêm giảm giá mới? Chỉ cần tạo class mới!
class EnterpriseDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.25;
  }
}
 
// Giảm giá theo mùa với logic tùy chỉnh
class SeasonalDiscount implements DiscountStrategy {
  constructor(private baseRate: number, private seasonalBonus: number) {}
 
  calculate(amount: number): number {
    return amount * (this.baseRate + this.seasonalBonus);
  }
}
 
// DiscountCalculator không bao giờ cần thay đổi
class DiscountCalculator {
  calculate(strategy: DiscountStrategy, amount: number): number {
    return strategy.calculate(amount);
  }
}
 
// Sử dụng
const calculator = new DiscountCalculator();
console.log(calculator.calculate(new RegularDiscount(), 100));    // 5
console.log(calculator.calculate(new VIPDiscount(), 100));        // 20
console.log(calculator.calculate(new EnterpriseDiscount(), 100)); // 25 - Không thay đổi calculator!
# Python - Áp dụng OCP
from abc import ABC, abstractmethod
 
class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, amount: float) -> float:
        pass
 
class RegularDiscount(DiscountStrategy):
    def calculate(self, amount: float) -> float:
        return amount * 0.05
 
class PremiumDiscount(DiscountStrategy):
    def calculate(self, amount: float) -> float:
        return amount * 0.10
 
class VIPDiscount(DiscountStrategy):
    def calculate(self, amount: float) -> float:
        return amount * 0.20
 
# Thêm giảm giá mới - không sửa code hiện có
class EnterpriseDiscount(DiscountStrategy):
    def calculate(self, amount: float) -> float:
        return amount * 0.25
 
class SeasonalDiscount(DiscountStrategy):
    def __init__(self, base_rate: float, seasonal_bonus: float):
        self._base_rate = base_rate
        self._seasonal_bonus = seasonal_bonus
 
    def calculate(self, amount: float) -> float:
        return amount * (self._base_rate + self._seasonal_bonus)
 
class DiscountCalculator:
    def calculate(self, strategy: DiscountStrategy, amount: float) -> float:
        return strategy.calculate(amount)
 
# Sử dụng
calculator = DiscountCalculator()
print(calculator.calculate(RegularDiscount(), 100))    # 5.0
print(calculator.calculate(VIPDiscount(), 100))        # 20.0
print(calculator.calculate(EnterpriseDiscount(), 100)) # 25.0
// Java - Áp dụng OCP
public interface DiscountStrategy {
    double calculate(double amount);
}
 
public class RegularDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.05;
    }
}
 
public class PremiumDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.10;
    }
}
 
public class VIPDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.20;
    }
}
 
// Giảm giá mới - không thay đổi class hiện có
public class EnterpriseDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.25;
    }
}
 
public class DiscountCalculator {
    public double calculate(DiscountStrategy strategy, double amount) {
        return strategy.calculate(amount);
    }
}

OCP trong Thực tế

Mẫu then chốt là: dùng abstraction (interfaces/abstract classes) làm điểm mở rộng. Khi yêu cầu thay đổi, bạn tạo implementation mới thay vì sửa đổi cái hiện có.

Ứng dụng OCP phổ biến:

  • Kiến trúc plugin — plugin mới không thay đổi core
  • Bộ xử lý thanh toán — phương thức thanh toán mới không sửa checkout
  • Định dạng xuất — loại export mới (PDF, CSV, XML) không thay đổi engine export
  • Kênh thông báo — kênh mới (Slack, SMS) không sửa service thông báo

L — Liskov Substitution Principle (LSP)

Subtypes phải có thể thay thế cho base types mà không làm thay đổi tính đúng đắn của chương trình.

Nếu code hoạt động với parent class, nó phải hoạt động với bất kỳ child class nào mà không có bất ngờ. Chúng ta đã đề cập đến điều này trong bài Đa hình và Interfaces—giờ hãy đi sâu hơn.

Vi phạm: Bài toán Square-Rectangle Kinh điển

// TypeScript - Vi phạm LSP
class Rectangle {
  constructor(protected width: number, protected height: number) {}
 
  setWidth(width: number): void {
    this.width = width;
  }
 
  setHeight(height: number): void {
    this.height = height;
  }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }
 
  // Override để duy trì tính bất biến của hình vuông
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Tác dụng phụ!
  }
 
  setHeight(height: number): void {
    this.height = height;
    this.width = height; // Tác dụng phụ!
  }
}
 
// Hàm này mong đợi hành vi của Rectangle
function resize(rect: Rectangle): void {
  rect.setWidth(10);
  rect.setHeight(5);
  // Với Rectangle: area phải là 50
  // Với Square: area là 25 (height set cả hai chiều thành 5)
  console.log(`Expected: 50, Got: ${rect.getArea()}`);
}
 
resize(new Rectangle(1, 1)); // Expected: 50, Got: 50 ✓
resize(new Square(1));        // Expected: 50, Got: 25 ✗ — Vi phạm LSP!

Tại sao không tốt? Code hoạt động đúng với Rectangle bị lỗi khi nhận Square. Subclass thay đổi hành vi mong đợi của parent.

Refactor: Thiết kế lại Hệ thống Phân cấp

// TypeScript - Tuân thủ LSP
 
// Interface chung cho mọi hình
interface Shape {
  getArea(): number;
}
 
// Value objects bất biến — không có setters để vi phạm
class Rectangle implements Shape {
  constructor(
    readonly width: number,
    readonly height: number
  ) {}
 
  getArea(): number {
    return this.width * this.height;
  }
 
  // Trả về instance mới thay vì thay đổi
  withWidth(width: number): Rectangle {
    return new Rectangle(width, this.height);
  }
 
  withHeight(height: number): Rectangle {
    return new Rectangle(this.width, height);
  }
}
 
class Square implements Shape {
  constructor(readonly size: number) {}
 
  getArea(): number {
    return this.size * this.size;
  }
 
  withSize(size: number): Square {
    return new Square(size);
  }
}
 
// Hoạt động với bất kỳ Shape nào — không bất ngờ
function printArea(shape: Shape): void {
  console.log(`Area: ${shape.getArea()}`);
}
 
printArea(new Rectangle(10, 5)); // Area: 50
printArea(new Square(7));         // Area: 49
# Python - Tuân thủ LSP
from abc import ABC, abstractmethod
 
class Shape(ABC):
    @abstractmethod
    def get_area(self) -> float:
        pass
 
class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height
 
    @property
    def width(self) -> float:
        return self._width
 
    @property
    def height(self) -> float:
        return self._height
 
    def get_area(self) -> float:
        return self._width * self._height
 
    def with_width(self, width: float) -> "Rectangle":
        return Rectangle(width, self._height)
 
    def with_height(self, height: float) -> "Rectangle":
        return Rectangle(self._width, height)
 
class Square(Shape):
    def __init__(self, size: float):
        self._size = size
 
    @property
    def size(self) -> float:
        return self._size
 
    def get_area(self) -> float:
        return self._size * self._size
 
    def with_size(self, size: float) -> "Square":
        return Square(size)
 
# Hoạt động với bất kỳ Shape nào
def print_area(shape: Shape) -> None:
    print(f"Area: {shape.get_area()}")
 
print_area(Rectangle(10, 5))  # Area: 50
print_area(Square(7))          # Area: 49
// Java - Tuân thủ LSP
public interface Shape {
    double getArea();
}
 
public record Rectangle(double width, double height) implements Shape {
    @Override
    public double getArea() {
        return width * height;
    }
 
    public Rectangle withWidth(double width) {
        return new Rectangle(width, this.height);
    }
 
    public Rectangle withHeight(double height) {
        return new Rectangle(this.width, height);
    }
}
 
public record Square(double size) implements Shape {
    @Override
    public double getArea() {
        return size * size;
    }
 
    public Square withSize(double size) {
        return new Square(size);
    }
}

Quy tắc Ngón tay cái cho LSP

  1. Không tăng cường preconditions — Subclass không nên yêu cầu nhiều hơn parent
  2. Không giảm yếu postconditions — Subclass phải đảm bảo ít nhất những gì parent hứa
  3. Bảo toàn invariants — Subclass không nên phá vỡ ràng buộc mà parent đảm bảo
  4. Không bất ngờ — Nếu code hoạt động với parent, nó phải hoạt động với child

Các Vi phạm LSP Phổ biến

Vi phạmVí dụCách sửa
Ném exception bất ngờReadOnlyList.add() ném UnsupportedOperationExceptionTách interface ReadableList và WritableList
Thay đổi ngữ nghĩa returnStack.pop() trả type khác Collection.remove()Không kế thừa từ abstraction không liên quan
Tác dụng phụ trong overridesSquare.setWidth() cũng thay đổi heightDùng immutable objects hoặc bỏ kế thừa
Bỏ qua hành vi parentOverride method rỗng (eat() không làm gì trong RobotWorker)Dùng interface segregation

I — Interface Segregation Principle (ISP)

Không client nào bị buộc phải phụ thuộc vào methods mà nó không dùng.

Tạo interfaces nhỏ, tập trung thay vì lớn, nguyên khối. Đây là về thiết kế contracts dựa trên vai trò.

Vi phạm: Fat Interface

// TypeScript - Vi phạm ISP
interface IDevice {
  print(document: string): void;
  scan(): string;
  fax(document: string, number: string): void;
  staple(document: string): void;
  copy(document: string): string;
}
 
// Máy in gia đình đơn giản không fax hay ghim được!
class HomePrinter implements IDevice {
  print(document: string): void {
    console.log(`Printing: ${document}`);
  }
 
  scan(): string {
    return "Scanned document";
  }
 
  fax(document: string, number: string): void {
    throw new Error("Home printer cannot fax!"); // Bị buộc implement!
  }
 
  staple(document: string): void {
    throw new Error("Home printer cannot staple!"); // Bị buộc implement!
  }
 
  copy(document: string): string {
    return `Copy of: ${document}`;
  }
}

Tại sao không tốt? HomePrinter bị buộc implement fax() và staple() dù không thể làm những việc đó. Bất kỳ code nào gọi fax() trên HomePrinter đều nhận runtime error.

Refactor: Interfaces Dựa trên Vai trò

// TypeScript - Áp dụng ISP
 
// Interfaces nhỏ, tập trung
interface IPrinter {
  print(document: string): void;
}
 
interface IScanner {
  scan(): string;
}
 
interface IFax {
  fax(document: string, number: string): void;
}
 
interface ICopier {
  copy(document: string): string;
}
 
interface IStapler {
  staple(document: string): void;
}
 
// Máy in nhà chỉ implement những gì nó làm được
class HomePrinter implements IPrinter, IScanner, ICopier {
  print(document: string): void {
    console.log(`Printing: ${document}`);
  }
 
  scan(): string {
    return "Scanned document";
  }
 
  copy(document: string): string {
    return `Copy of: ${document}`;
  }
}
 
// Máy văn phòng implement mọi thứ
class OfficeMachine implements IPrinter, IScanner, IFax, ICopier, IStapler {
  print(document: string): void {
    console.log(`Printing: ${document}`);
  }
 
  scan(): string {
    return "High-res scanned document";
  }
 
  fax(document: string, number: string): void {
    console.log(`Faxing to ${number}: ${document}`);
  }
 
  copy(document: string): string {
    return `Office copy of: ${document}`;
  }
 
  staple(document: string): void {
    console.log(`Stapling: ${document}`);
  }
}
 
// Hàm chỉ nhận những gì cần
function printDocument(printer: IPrinter, doc: string): void {
  printer.print(doc);
}
 
function scanAndCopy(device: IScanner & ICopier, doc: string): void {
  const scanned = device.scan();
  const copied = device.copy(doc);
  console.log(`Scanned: ${scanned}, Copied: ${copied}`);
}
 
// Cả hai đều hoạt động với printDocument
printDocument(new HomePrinter(), "Report.pdf");
printDocument(new OfficeMachine(), "Report.pdf");
 
// Cả hai đều hoạt động với scanAndCopy
scanAndCopy(new HomePrinter(), "Invoice.pdf");
scanAndCopy(new OfficeMachine(), "Invoice.pdf");
# Python - Áp dụng ISP
from abc import ABC, abstractmethod
 
class IPrinter(ABC):
    @abstractmethod
    def print_document(self, document: str) -> None:
        pass
 
class IScanner(ABC):
    @abstractmethod
    def scan(self) -> str:
        pass
 
class IFax(ABC):
    @abstractmethod
    def fax(self, document: str, number: str) -> None:
        pass
 
class ICopier(ABC):
    @abstractmethod
    def copy(self, document: str) -> str:
        pass
 
# Máy in nhà - chỉ in, scan, copy
class HomePrinter(IPrinter, IScanner, ICopier):
    def print_document(self, document: str) -> None:
        print(f"Printing: {document}")
 
    def scan(self) -> str:
        return "Scanned document"
 
    def copy(self, document: str) -> str:
        return f"Copy of: {document}"
 
# Máy văn phòng - mọi thứ
class OfficeMachine(IPrinter, IScanner, IFax, ICopier):
    def print_document(self, document: str) -> None:
        print(f"Printing: {document}")
 
    def scan(self) -> str:
        return "High-res scanned document"
 
    def fax(self, document: str, number: str) -> None:
        print(f"Faxing to {number}: {document}")
 
    def copy(self, document: str) -> str:
        return f"Office copy of: {document}"
 
# Hàm chỉ nhận những gì cần
def print_doc(printer: IPrinter, doc: str) -> None:
    printer.print_document(doc)
 
def scan_and_copy(device: IScanner, doc: str) -> None:
    scanned = device.scan()
    print(f"Scanned: {scanned}")
 
print_doc(HomePrinter(), "Report.pdf")
print_doc(OfficeMachine(), "Report.pdf")
// Java - Áp dụng ISP
public interface IPrinter {
    void print(String document);
}
 
public interface IScanner {
    String scan();
}
 
public interface IFax {
    void fax(String document, String number);
}
 
public interface ICopier {
    String copy(String document);
}
 
// Máy in nhà - chỉ những gì nó làm được
public class HomePrinter implements IPrinter, IScanner, ICopier {
    @Override
    public void print(String document) {
        System.out.println("Printing: " + document);
    }
 
    @Override
    public String scan() {
        return "Scanned document";
    }
 
    @Override
    public String copy(String document) {
        return "Copy of: " + document;
    }
}
 
// Máy văn phòng - đầy đủ chức năng
public class OfficeMachine implements IPrinter, IScanner, IFax, ICopier {
    @Override
    public void print(String document) {
        System.out.println("Printing: " + document);
    }
 
    @Override
    public String scan() {
        return "High-res scanned document";
    }
 
    @Override
    public void fax(String document, String number) {
        System.out.println("Faxing to " + number + ": " + document);
    }
 
    @Override
    public String copy(String document) {
        return "Office copy of: " + document;
    }
}

Lợi ích ISP

  • Không có dead code — Classes chỉ implement methods mà chúng thực sự hỗ trợ
  • An toàn compile-time — Không thể vô tình gọi fax() trên HomePrinter nếu hàm chỉ nhận IPrinter
  • Dễ testing — Mock chỉ interface cần, không phải 10 methods
  • Ý định rõ ràng — Function signatures truyền đạt chính xác capabilities cần thiết

D — Dependency Inversion Principle (DIP)

Module cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai nên phụ thuộc vào abstractions.

Đây thường là nguyên tắc SOLID có tác động lớn nhất. Thay vì UserService trực tiếp tạo PostgresDatabase, nó phụ thuộc vào interface IDatabase. Implementation cụ thể được inject từ bên ngoài.

Vi phạm: Tight Coupling

// TypeScript - Vi phạm DIP
class MySQLDatabase {
  save(data: string): void {
    console.log(`Saving to MySQL: ${data}`);
  }
 
  query(sql: string): string[] {
    console.log(`Querying MySQL: ${sql}`);
    return [];
  }
}
 
class StripePayment {
  charge(amount: number): boolean {
    console.log(`Charging $${amount} via Stripe`);
    return true;
  }
}
 
class SmtpEmailer {
  send(to: string, subject: string, body: string): void {
    console.log(`SMTP sending to ${to}: ${subject}`);
  }
}
 
// Module cấp cao phụ thuộc trực tiếp vào module cấp thấp
class OrderService {
  private db = new MySQLDatabase();        // Dependency cứng
  private payment = new StripePayment();   // Dependency cứng
  private emailer = new SmtpEmailer();     // Dependency cứng
 
  placeOrder(userId: string, amount: number): void {
    this.payment.charge(amount);
    this.db.save(`Order for ${userId}: $${amount}`);
    this.emailer.send(userId, "Order Confirmation", `Your order of $${amount} is confirmed`);
  }
}

Tại sao không tốt? Muốn đổi từ MySQL sang PostgreSQL? Viết lại OrderService. Muốn test không cần Stripe? Không được. Muốn thêm SendGrid thay SMTP? Sửa OrderService. Mỗi thay đổi infrastructure buộc thay đổi business logic.

Refactor: Phụ thuộc vào Abstractions

// TypeScript - Áp dụng DIP
 
// Abstractions (interfaces)
interface IDatabase {
  save(data: string): void;
  query(sql: string): string[];
}
 
interface IPaymentGateway {
  charge(amount: number): boolean;
}
 
interface IEmailService {
  send(to: string, subject: string, body: string): void;
}
 
// Implementations cấp thấp
class MySQLDatabase implements IDatabase {
  save(data: string): void {
    console.log(`Saving to MySQL: ${data}`);
  }
  query(sql: string): string[] {
    console.log(`Querying MySQL: ${sql}`);
    return [];
  }
}
 
class PostgresDatabase implements IDatabase {
  save(data: string): void {
    console.log(`Saving to PostgreSQL: ${data}`);
  }
  query(sql: string): string[] {
    console.log(`Querying PostgreSQL: ${sql}`);
    return [];
  }
}
 
class StripePayment implements IPaymentGateway {
  charge(amount: number): boolean {
    console.log(`Charging $${amount} via Stripe`);
    return true;
  }
}
 
class PayPalPayment implements IPaymentGateway {
  charge(amount: number): boolean {
    console.log(`Charging $${amount} via PayPal`);
    return true;
  }
}
 
class SmtpEmailer implements IEmailService {
  send(to: string, subject: string, body: string): void {
    console.log(`SMTP sending to ${to}: ${subject}`);
  }
}
 
class SendGridEmailer implements IEmailService {
  send(to: string, subject: string, body: string): void {
    console.log(`SendGrid sending to ${to}: ${subject}`);
  }
}
 
// Module cấp cao phụ thuộc vào abstractions
class OrderService {
  constructor(
    private db: IDatabase,
    private payment: IPaymentGateway,
    private emailer: IEmailService
  ) {}
 
  placeOrder(userId: string, amount: number): void {
    this.payment.charge(amount);
    this.db.save(`Order for ${userId}: $${amount}`);
    this.emailer.send(userId, "Order Confirmation", `Your order of $${amount} is confirmed`);
  }
}
 
// Sử dụng — hoán đổi implementations không cần thay đổi OrderService
const orderService = new OrderService(
  new PostgresDatabase(),  // Đổi DB? Thay ở đây, không trong OrderService
  new StripePayment(),     // Đổi payment? Thay ở đây
  new SendGridEmailer()    // Đổi email? Thay ở đây
);
 
orderService.placeOrder("user123", 99.99);
 
// Cho testing — dùng mock implementations
class MockDatabase implements IDatabase {
  savedData: string[] = [];
  save(data: string): void { this.savedData.push(data); }
  query(sql: string): string[] { return []; }
}
 
class MockPayment implements IPaymentGateway {
  charged = false;
  charge(amount: number): boolean { this.charged = true; return true; }
}
 
class MockEmailer implements IEmailService {
  sentEmails: string[] = [];
  send(to: string, subject: string, body: string): void { this.sentEmails.push(to); }
}
 
// Dễ dàng test!
const testService = new OrderService(
  new MockDatabase(),
  new MockPayment(),
  new MockEmailer()
);
# Python - Áp dụng DIP
from abc import ABC, abstractmethod
 
# Abstractions
class IDatabase(ABC):
    @abstractmethod
    def save(self, data: str) -> None:
        pass
 
    @abstractmethod
    def query(self, sql: str) -> list[str]:
        pass
 
class IPaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: float) -> bool:
        pass
 
class IEmailService(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str) -> None:
        pass
 
# Concrete implementations
class PostgresDatabase(IDatabase):
    def save(self, data: str) -> None:
        print(f"Saving to PostgreSQL: {data}")
 
    def query(self, sql: str) -> list[str]:
        print(f"Querying PostgreSQL: {sql}")
        return []
 
class StripePayment(IPaymentGateway):
    def charge(self, amount: float) -> bool:
        print(f"Charging ${amount} via Stripe")
        return True
 
class SendGridEmailer(IEmailService):
    def send(self, to: str, subject: str, body: str) -> None:
        print(f"SendGrid sending to {to}: {subject}")
 
# Module cấp cao phụ thuộc vào abstractions
class OrderService:
    def __init__(
        self,
        db: IDatabase,
        payment: IPaymentGateway,
        emailer: IEmailService
    ):
        self._db = db
        self._payment = payment
        self._emailer = emailer
 
    def place_order(self, user_id: str, amount: float) -> None:
        self._payment.charge(amount)
        self._db.save(f"Order for {user_id}: ${amount}")
        self._emailer.send(
            user_id,
            "Order Confirmation",
            f"Your order of ${amount} is confirmed"
        )
 
# Hoán đổi implementations thoải mái
service = OrderService(
    db=PostgresDatabase(),
    payment=StripePayment(),
    emailer=SendGridEmailer()
)
service.place_order("user123", 99.99)
// Java - Áp dụng DIP
public interface IDatabase {
    void save(String data);
    List<String> query(String sql);
}
 
public interface IPaymentGateway {
    boolean charge(double amount);
}
 
public interface IEmailService {
    void send(String to, String subject, String body);
}
 
// Implementations
public class PostgresDatabase implements IDatabase {
    @Override
    public void save(String data) {
        System.out.println("Saving to PostgreSQL: " + data);
    }
 
    @Override
    public List<String> query(String sql) {
        System.out.println("Querying PostgreSQL: " + sql);
        return List.of();
    }
}
 
public class StripePayment implements IPaymentGateway {
    @Override
    public boolean charge(double amount) {
        System.out.println("Charging $" + amount + " via Stripe");
        return true;
    }
}
 
// Module cấp cao phụ thuộc vào abstractions
public class OrderService {
    private final IDatabase db;
    private final IPaymentGateway payment;
    private final IEmailService emailer;
 
    public OrderService(IDatabase db, IPaymentGateway payment, IEmailService emailer) {
        this.db = db;
        this.payment = payment;
        this.emailer = emailer;
    }
 
    public void placeOrder(String userId, double amount) {
        payment.charge(amount);
        db.save("Order for " + userId + ": $" + amount);
        emailer.send(userId, "Order Confirmation",
            "Your order of $" + amount + " is confirmed");
    }
}

DIP Cho phép

  • Khả năng test — Inject mocks/fakes cho unit testing
  • Linh hoạt — Hoán đổi implementations (MySQL → PostgreSQL) không chạm business logic
  • Modular — Teams có thể làm việc trên implementations khác nhau độc lập
  • Tích hợp framework — Spring, Nest.js và FastAPI đều dùng DI containers dựa trên nguyên tắc này

Ví dụ Thực tế: Hệ thống Đơn hàng E-Commerce

Hãy xem cả năm nguyên tắc SOLID hoạt động cùng nhau trong kịch bản thực tế:

// TypeScript - Áp dụng Tất cả Nguyên tắc SOLID
 
// === Interfaces (ISP - nhỏ, tập trung) ===
 
interface IOrderValidator {
  validate(order: Order): ValidationResult;
}
 
interface IInventoryChecker {
  checkAvailability(productId: string, quantity: number): boolean;
  reserve(productId: string, quantity: number): void;
}
 
interface IPaymentProcessor {
  process(amount: number, method: PaymentMethod): PaymentResult;
}
 
interface IOrderRepository {
  save(order: Order): void;
  findById(id: string): Order | null;
}
 
interface INotificationSender {
  send(userId: string, message: string): void;
}
 
// === Data Types ===
 
interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  total: number;
  status: "pending" | "confirmed" | "shipped" | "delivered";
}
 
interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}
 
interface PaymentMethod {
  type: string;
}
 
interface PaymentResult {
  success: boolean;
  transactionId: string;
}
 
interface ValidationResult {
  valid: boolean;
  errors: string[];
}
 
// === Validators (OCP - thêm validators mới không cần thay đổi cái cũ) ===
 
class MinimumAmountValidator implements IOrderValidator {
  constructor(private minimumAmount: number) {}
 
  validate(order: Order): ValidationResult {
    if (order.total < this.minimumAmount) {
      return { valid: false, errors: [`Minimum order amount is $${this.minimumAmount}`] };
    }
    return { valid: true, errors: [] };
  }
}
 
class ItemLimitValidator implements IOrderValidator {
  constructor(private maxItems: number) {}
 
  validate(order: Order): ValidationResult {
    if (order.items.length > this.maxItems) {
      return { valid: false, errors: [`Maximum ${this.maxItems} items per order`] };
    }
    return { valid: true, errors: [] };
  }
}
 
// Validator mới — không sửa code hiện có (OCP)
class FraudCheckValidator implements IOrderValidator {
  validate(order: Order): ValidationResult {
    if (order.total > 10000) {
      return { valid: false, errors: ["Order exceeds fraud threshold — manual review required"] };
    }
    return { valid: true, errors: [] };
  }
}
 
// === Composite Validator (SRP - chỉ validate) ===
 
class CompositeOrderValidator implements IOrderValidator {
  constructor(private validators: IOrderValidator[]) {}
 
  validate(order: Order): ValidationResult {
    const allErrors: string[] = [];
 
    for (const validator of this.validators) {
      const result = validator.validate(order);
      if (!result.valid) {
        allErrors.push(...result.errors);
      }
    }
 
    return {
      valid: allErrors.length === 0,
      errors: allErrors
    };
  }
}
 
// === Order Service (SRP - chỉ điều phối, DIP - phụ thuộc abstractions) ===
 
class OrderService {
  constructor(
    private validator: IOrderValidator,
    private inventory: IInventoryChecker,
    private payment: IPaymentProcessor,
    private repository: IOrderRepository,
    private notification: INotificationSender
  ) {}
 
  placeOrder(order: Order, paymentMethod: PaymentMethod): PaymentResult | null {
    // Bước 1: Validate
    const validation = this.validator.validate(order);
    if (!validation.valid) {
      console.log(`Validation failed: ${validation.errors.join(", ")}`);
      return null;
    }
 
    // Bước 2: Kiểm tra kho
    for (const item of order.items) {
      if (!this.inventory.checkAvailability(item.productId, item.quantity)) {
        console.log(`Product ${item.productId} not available`);
        return null;
      }
    }
 
    // Bước 3: Xử lý thanh toán
    const paymentResult = this.payment.process(order.total, paymentMethod);
    if (!paymentResult.success) {
      console.log("Payment failed");
      return null;
    }
 
    // Bước 4: Đặt trước kho
    for (const item of order.items) {
      this.inventory.reserve(item.productId, item.quantity);
    }
 
    // Bước 5: Lưu đơn hàng
    order.status = "confirmed";
    this.repository.save(order);
 
    // Bước 6: Thông báo
    this.notification.send(order.userId, `Order ${order.id} confirmed!`);
 
    return paymentResult;
  }
}

Nguyên tắc SOLID trong Ví dụ này

Nguyên tắcNơi Áp dụng
SRPMỗi class có một việc: MinimumAmountValidator validate amounts, OrderService điều phối, CompositeOrderValidator kết hợp validators
OCPThêm FraudCheckValidator không sửa validators hiện có hay OrderService
LSPTất cả IOrderValidator implementations có thể thay thế — CompositeOrderValidator hoạt động với bất kỳ tổ hợp nào
ISPInterfaces riêng cho validation, inventory, payment, storage, notification — mỗi client chỉ phụ thuộc vào cái cần
DIPOrderService phụ thuộc interfaces, không phải PostgresDB hay StripePayment trực tiếp

Các Nguyên tắc SOLID Kết nối Nhau

Năm nguyên tắc không tách biệt—chúng bổ trợ nhau:

  • SRP tạo classes tập trung → dễ mở rộng hơn (OCP)
  • OCP dựa vào polymorphism → yêu cầu thay thế đúng (LSP)
  • ISP tạo interfaces nhỏ → là abstractions mà DIP phụ thuộc vào
  • DIP cho phép hoán đổi implementations → hoạt động nhờ LSP

Bảng Tra cứu Nhanh SOLID

Nguyên tắcMột câuCâu hỏi Then chốt
S — Single ResponsibilityMột class, một việc"Class này có nhiều hơn một lý do để thay đổi không?"
O — Open/ClosedThêm code mới, không sửa code cũ"Tôi có thể thêm tính năng này mà không sửa classes hiện có?"
L — Liskov SubstitutionSubclasses không gây bất ngờ"Thay parent bằng child có phá vỡ gì không?"
I — Interface SegregationInterfaces nhỏ, không phình"Clients có bị buộc implement methods không dùng?"
D — Dependency InversionPhụ thuộc abstractions"Module cấp cao này có biết chi tiết implementation?"

Khi nào KHÔNG Áp dụng SOLID

Nguyên tắc SOLID là hướng dẫn, không phải giáo điều. Áp dụng quá mức có thể dẫn đến:

  • Over-engineering — Tạo interfaces cho classes sẽ không bao giờ có implementation thứ hai
  • Bùng nổ abstraction — 50 interfaces nhỏ cho app CRUD đơn giản
  • Tê liệt phân tích — Dành nhiều thời gian thiết kế hơn xây dựng

Quy tắc ngón tay cái:

  1. Bắt đầu đơn giản — Thêm abstractions khi cần implementation thứ hai, không phải trước
  2. Theo sự đau đớn — Nếu thay đổi một thứ phá vỡ thứ khác, đó là vi phạm SOLID đáng sửa
  3. Xem xét phạm vi — Script 200 dòng không cần DIP; hệ thống 200,000 dòng thì cần
  4. Refactor hướng SOLID — Dễ hơn nhiều khi extract interfaces từ code đang hoạt động thay vì dự đoán abstraction hoàn hảo từ đầu

Tóm tắt và Điểm chính

Năm Nguyên tắc

✅ SRP — Mỗi class chỉ nên có một lý do để thay đổi
✅ OCP — Mở rộng hành vi qua class mới, không sửa cái hiện có
✅ LSP — Subtypes phải tôn trọng contracts của parent type
✅ ISP — Nhiều interfaces nhỏ tốt hơn một interface lớn
✅ DIP — Phụ thuộc abstractions (interfaces), không phải concrete implementations

SOLID Cho phép

  • Code testable — DIP cho phép inject mocks
  • Code extensible — OCP cho phép thêm tính năng không phá vỡ
  • Code readable — SRP cho mỗi class mục đích rõ ràng
  • Kế thừa an toàn — LSP ngăn bugs tinh vi
  • Contracts sạch — ISP đảm bảo classes chỉ implement cái cần dùng

SOLID Là Nền tảng cho Design Patterns

Mọi design pattern bạn sẽ học tiếp theo—Singleton, Factory, Strategy, Observer—đều xây dựng trên nguyên tắc SOLID. Hiểu SOLID khiến design patterns trở nên trực giác thay vì phép thuật.


Bài tập Thực hành

  1. Refactor Report Generator: Bạn có class ReportGenerator tạo reports, format HTML/PDF/CSV, lưu vào disk, và email kết quả. Áp dụng SRP để tách concerns, sau đó OCP để format extensible.

  2. Thiết kế Payment System: Xây dựng hệ thống xử lý thanh toán với nhiều gateway (Stripe, PayPal, Bank Transfer). Áp dụng DIP để checkout service không biết gateway nào được dùng. Áp dụng ISP tách interface refundable vs non-refundable.

  3. Sửa Inheritance Hierarchy: Bạn có Bird với method fly(), và Penguin extends Bird. Áp dụng LSP và ISP để sửa hierarchy sao cho penguins không "fly" và không code nào bị lỗi.


Tiếp theo

Với nguyên tắc SOLID trong tay, bạn sẵn sàng học design patterns—các giải pháp đã được chứng minh cho các vấn đề thiết kế lặp lại. Trong bài tiếp theo, chúng ta sẽ bắt đầu với creational patterns:

  • Singleton Pattern — Đảm bảo class chỉ có một instance
  • Khi nào dùng và khi nào tránh
  • Implementations an toàn thread
  • Các giải pháp thay thế Singleton

Tiếp tục hành trình OOP: Singleton Pattern


Tài nguyên Bổ sung

Các Bài Viết Trước trong Series

Bài Viết Liên quan

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.