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
- Hoàn thành Bốn Trụ Cột của OOP
- Hoàn thành Encapsulation và Information Hiding
- Hoàn thành Kế thừa và Composition
- Hoàn thành Đa hình và Interfaces
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ó SOLID | Có SOLID |
|---|---|---|
| Thêm tính năng | Sửa class hiện có, rủi ro phá vỡ code | Thêm class mới, code hiện có không đổi |
| Testing | Không thể test một thứ mà không setup mọi thứ | Mỗi class test được độc lập |
| Hiểu code | Một class làm 10 việc | Mỗi class có mục đích rõ ràng |
| Thay đổi yêu cầu | Thay đổi lan truyền khắp codebase | Thay đổi cô lập trong module cụ thể |
| Làm việc nhóm | Mọi người sửa cùng file | Cá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:
- 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
- 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
- Class có khó đặt tên không? — Nếu cần tên chung chung như
ManagerhayHandler, 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
- Không tăng cường preconditions — Subclass không nên yêu cầu nhiều hơn parent
- Không giảm yếu postconditions — Subclass phải đảm bảo ít nhất những gì parent hứa
- Bảo toàn invariants — Subclass không nên phá vỡ ràng buộc mà parent đảm bảo
- 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ạm | Ví dụ | Cách sửa |
|---|---|---|
| Ném exception bất ngờ | ReadOnlyList.add() ném UnsupportedOperationException | Tách interface ReadableList và WritableList |
| Thay đổi ngữ nghĩa return | Stack.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 overrides | Square.setWidth() cũng thay đổi height | Dùng immutable objects hoặc bỏ kế thừa |
| Bỏ qua hành vi parent | Override 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ênHomePrinternếu hàm chỉ nhậnIPrinter - 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ắc | Nơi Áp dụng |
|---|---|
| SRP | Mỗi class có một việc: MinimumAmountValidator validate amounts, OrderService điều phối, CompositeOrderValidator kết hợp validators |
| OCP | Thêm FraudCheckValidator không sửa validators hiện có hay OrderService |
| LSP | Tất cả IOrderValidator implementations có thể thay thế — CompositeOrderValidator hoạt động với bất kỳ tổ hợp nào |
| ISP | Interfaces riêng cho validation, inventory, payment, storage, notification — mỗi client chỉ phụ thuộc vào cái cần |
| DIP | OrderService 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ắc | Một câu | Câu hỏi Then chốt |
|---|---|---|
| S — Single Responsibility | Một class, một việc | "Class này có nhiều hơn một lý do để thay đổi không?" |
| O — Open/Closed | Thê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 Substitution | Subclasses không gây bất ngờ | "Thay parent bằng child có phá vỡ gì không?" |
| I — Interface Segregation | Interfaces nhỏ, không phình | "Clients có bị buộc implement methods không dùng?" |
| D — Dependency Inversion | Phụ 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:
- Bắt đầu đơn giản — Thêm abstractions khi cần implementation thứ hai, không phải trước
- Theo sự đau đớn — Nếu thay đổi một thứ phá vỡ thứ khác, đó là vi phạm SOLID đáng sửa
- 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
- 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
-
Refactor Report Generator: Bạn có class
ReportGeneratortạ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. -
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.
-
Sửa Inheritance Hierarchy: Bạn có
Birdvới methodfly(), 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
- OOP & Design Patterns Roadmap - Lộ trình học tập hoàn chỉnh
- Bốn Trụ Cột của OOP - Nền tảng OOP
- Classes, Objects và Abstraction - Các khối xây dựng của OOP
- Encapsulation và Information Hiding - Bảo vệ tính toàn vẹn dữ liệu
- Kế thừa và Composition - Chiến lược tái sử dụng code
- Đa hình và Interfaces - Một interface, nhiều hình thức
Bài Viết Liên quan
- Java Phase 2: Lập trình Hướng Đối tượng - OOP trong Java
- Python Phase 2: OOP & Tính năng Nâng cao - OOP trong Python
- TypeScript Advanced Types Deep Dive - Tính năng type system nâng cao
📬 Đă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.