1. SOLID là gì?
SOLID là 5 nguyên lý thiết kế OOP được Robert C. Martin (Uncle Bob) đặt tên vào ~2000, dựa trên các bài viết từ thập kỷ trước đó. Mục đích: giúp code dễ thay đổi, dễ test, dễ mở rộng.
| Letter | Tên | Một câu |
|---|---|---|
| S | Single Responsibility Principle | 1 class chỉ có 1 lý do để thay đổi. |
| O | Open/Closed Principle | Mở để mở rộng, đóng để sửa đổi. |
| L | Liskov Substitution Principle | Subtype phải thay thế được supertype. |
| I | Interface Segregation Principle | Nhiều interface nhỏ tốt hơn 1 interface to. |
| D | Dependency Inversion Principle | Phụ thuộc abstraction, không phụ thuộc concrete. |
"SOLID là heuristic, không phải luật. Đừng áp dụng cứng nhắc — dùng để đặt câu hỏi khi viết/review code: 'Class này có vi phạm SRP không? Nếu có, có đáng tách?'"
2. S — Single Responsibility Principle (SRP)
"A class should have only one reason to change."
Mỗi class chỉ phục vụ một stakeholder, một lý do để thay đổi. Nếu thay đổi chính sách kế toán làm bạn phải sửa class quản lý report PDF — class đó vi phạm SRP.
2.1. Ví dụ vi phạm
// ❌ Class này có 3 lý do để thay đổi:
class Employee {
constructor(public name: string, public hours: number, public rate: number) {}
// 1. Lý do business logic (tính lương đổi → sửa)
calculatePay(): number {
return this.hours * this.rate;
}
// 2. Lý do persistence (đổi DB → sửa)
save(): void {
db.query('INSERT INTO employees ...');
}
// 3. Lý do reporting (format đổi → sửa)
toReport(): string {
return `<html><h1>${this.name}</h1>...</html>`;
}
}
3 stakeholder khác nhau (CFO, DBA, Marketing) muốn thay đổi 3 method. Class này có 3 lý do để thay đổi → SRP fail.
2.2. Refactor theo SRP
// ✓ Tách trách nhiệm:
class Employee {
constructor(public name: string, public hours: number, public rate: number) {}
}
class PayCalculator {
calculate(emp: Employee): number {
return emp.hours * emp.rate;
}
}
class EmployeeRepository {
save(emp: Employee): Promise<void> {
return db.query('INSERT INTO employees ...');
}
}
class EmployeeReporter {
toHtml(emp: Employee): string {
return `<html><h1>${emp.name}</h1>...</html>`;
}
}
Mỗi class giờ có đúng 1 lý do để đổi. Test cũng dễ — mock từng phần độc lập.
2.3. Tinh thần SRP
SRP không phải "1 class chỉ có 1 method". Một class có nhiều method là OK, miễn các method cùng lý do thay đổi.
Bob Martin diễn giải hiện đại:
"Gather together those things that change for the same reasons. Separate those things that change for different reasons."
UserNameValidator, UserEmailValidator, UserPasswordValidator thành 3 class.
Đó vẫn là 1 lý do thay đổi (validation rules cho User). 1 class UserValidator đủ.
3. O — Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
Có thể thêm hành vi mới bằng cách thêm code, không phải sửa code cũ. Code cũ đã test, đã chạy production — đụng vào là rủi ro. Mở rộng qua subclass hoặc plugin point.
3.1. Vi phạm OCP
// ❌ Thêm shape mới phải sửa class này:
class AreaCalculator {
calculate(shape: any): number {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
} else if (shape.type === 'triangle') {
return shape.base * shape.height / 2;
}
// Pentagon? → sửa hàm này, build lại, test lại, deploy lại.
return 0;
}
}
3.2. Refactor theo OCP
// ✓ Open for extension, closed for modification:
abstract class Shape {
abstract area(): number;
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area() { return Math.PI * this.radius ** 2; }
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) { super(); }
area() { return this.w * this.h; }
}
class AreaCalculator {
// Hàm này KHÔNG cần sửa khi thêm shape:
total(shapes: Shape[]): number {
return shapes.reduce((sum, s) => sum + s.area(), 0);
}
}
// Thêm Pentagon? Chỉ cần thêm class mới:
class Pentagon extends Shape {
constructor(private side: number) { super(); }
area() { return (5 * this.side ** 2) / (4 * Math.tan(Math.PI / 5)); }
}
3.3. OCP qua plugin / strategy
// Pricing rule có thể mở rộng qua plugin:
interface PricingRule {
apply(order: Order): Money;
}
class StandardPricing implements PricingRule {
apply(o: Order) { return o.subtotal(); }
}
class DiscountForVIP implements PricingRule {
apply(o: Order) {
return o.customer.isVIP() ? o.subtotal().multiply(0.9) : o.subtotal();
}
}
class HolidayPromo implements PricingRule {
apply(o: Order) {
return isHoliday() ? o.subtotal().subtract(Money.of(10)) : o.subtotal();
}
}
class PricingEngine {
constructor(private rules: PricingRule[]) {}
// Thêm rule mới chỉ cần inject — KHÔNG sửa engine:
finalPrice(o: Order) {
return this.rules.reduce((p, r) => r.apply(o), o.subtotal());
}
}
4. L — Liskov Substitution Principle (LSP)
Barbara Liskov, 1987:
"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the desirable properties of the program."
Nói cách khác: subclass phải hoạt động giống superclass đến mức code dùng superclass không nhận ra khi được pass subclass. Nghe đơn giản nhưng vi phạm liên tục, đặc biệt với inheritance "khôn lỏi".
4.1. Ví dụ kinh điển: Rectangle / Square
class Rectangle {
protected width: number = 0;
protected height: number = 0;
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
// "Hình vuông IS-A hình chữ nhật" → kế thừa, đúng không?
class Square extends Rectangle {
setWidth(w: number) { this.width = w; this.height = w; } // ép cả 2
setHeight(h: number) { this.width = h; this.height = h; }
}
// Code dùng Rectangle:
function expandWidth(rect: Rectangle) {
rect.setWidth(10);
rect.setHeight(5);
console.assert(rect.area() === 50); // ❌ Fails với Square!
}
expandWidth(new Square()); // area = 25, không phải 50
Square vi phạm LSP — nó không "thay thế" được Rectangle vì hành vi khác. Toán học "Square is a Rectangle" đúng, nhưng behavioral subtyping không bằng conceptual subtyping.
Sửa: làm Rectangle và Square là 2 class riêng, hoặc dùng immutable (return new instance):
// ✓ Immutable — không có setter, không vi phạm
abstract class Shape { abstract area(): number; }
class Rectangle extends Shape {
constructor(public readonly w: number, public readonly h: number) { super(); }
area() { return this.w * this.h; }
}
class Square extends Shape {
constructor(public readonly side: number) { super(); }
area() { return this.side ** 2; }
}
4.2. Quy tắc LSP cụ thể
Subclass không được:
- Strengthen pre-condition — yêu cầu nhiều hơn parent. Ví dụ parent chấp nhận negative number, child throw.
- Weaken post-condition — đảm bảo ít hơn parent. Ví dụ parent đảm bảo trả non-null, child trả null.
- Throw exception kiểu mới không thuộc hierarchy của parent.
- Change invariant mà parent đảm bảo (vd: balance >= 0).
4.3. Ví dụ: bird / penguin
class Bird {
fly() { console.log('flying'); }
}
class Penguin extends Bird {
fly() { throw new Error("Penguins can't fly"); } // ❌ vi phạm LSP
}
// Sửa: tách hierarchy đúng với hành vi
abstract class Bird { abstract eat(): void; }
abstract class FlyingBird extends Bird { abstract fly(): void; }
class Sparrow extends FlyingBird { fly() {} eat() {} }
class Penguin extends Bird { eat() {} swim() {} }
5. I — Interface Segregation Principle (ISP)
"Clients should not be forced to depend upon interfaces that they do not use."
Thay vì 1 interface "kitchen sink" với 20 method, tạo nhiều interface nhỏ chuyên biệt. Class implement chỉ cần implement những interface chúng thực sự cần.
5.1. Vi phạm ISP
// ❌ "Fat" interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class Human implements Worker {
work() { /* ... */ }
eat() { /* ... */ }
sleep(){ /* ... */ }
}
class Robot implements Worker {
work() { /* ... */ }
eat() { throw new Error('Robots do not eat'); } // ❌ phải implement vô nghĩa
sleep(){ throw new Error('Robots do not sleep'); }
}
5.2. Refactor — tách interface
// ✓ Nhiều interface nhỏ, role-based
interface Workable { work(): void; }
interface Eatable { eat(): void; }
interface Sleepable { sleep(): void; }
class Human implements Workable, Eatable, Sleepable {
work() {} eat() {} sleep() {}
}
class Robot implements Workable {
work() {} // chỉ implement cái cần
}
// Function chỉ cần Workable:
function manage(w: Workable) { w.work(); }
manage(new Human()); // ✓
manage(new Robot()); // ✓
5.3. ISP trong thực tế
Pattern phổ biến: tách interface theo role/capability:
interface Readable<T> {
read(): Promise<T>;
}
interface Writable<T> {
write(data: T): Promise<void>;
}
interface Deletable {
delete(id: string): Promise<void>;
}
// Repository thường impl cả 3
class FileRepository<T> implements Readable<T>, Writable<T>, Deletable { /* ... */ }
// ReadOnly view chỉ cần Readable:
class CachedReader<T> implements Readable<T> { /* ... */ }
// Function chỉ cần read:
async function backup<T>(src: Readable<T>) { return src.read(); }
backup(new CachedReader()); // ✓ — không cần Writable/Deletable
Lợi: client function chỉ phụ thuộc minimal interface → mock dễ hơn, coupling thấp.
6. D — Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."
Module high-level (business logic) không nên import trực tiếp module low-level (DB, file, HTTP). Thay vào đó, cả hai phụ thuộc vào abstraction (interface). Abstraction được định nghĩa bởi high-level module (theo nhu cầu của nó), low-level implement.
6.1. Vi phạm DIP
// ❌ OrderService phụ thuộc trực tiếp class concrete
import { PostgresClient } from './pg';
import { SendgridClient } from './sendgrid';
class OrderService {
private db = new PostgresClient();
private email = new SendgridClient();
async checkout(order: Order) {
await this.db.query('INSERT ...');
await this.email.send(order.customer.email, 'Order confirmed');
}
}
Vấn đề:
- Đổi DB từ Postgres sang MySQL? Phải sửa OrderService.
- Test? Phải mock thư viện postgres và sendgrid (khó).
- Logic business "lệ thuộc" công nghệ.
6.2. Refactor theo DIP
// ✓ Domain định nghĩa abstraction theo nhu cầu của nó:
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
interface NotificationSender {
send(to: string, subject: string, body: string): Promise<void>;
}
// High-level module — không biết Postgres hay Sendgrid:
class OrderService {
constructor(
private orders: OrderRepository,
private mailer: NotificationSender
) {}
async checkout(order: Order) {
await this.orders.save(order);
await this.mailer.send(order.customer.email, 'Confirmed', '...');
}
}
// Low-level module — implement abstraction:
class PostgresOrderRepository implements OrderRepository {
async save(o: Order) { /* SQL */ }
async findById(id: string) { return null; }
}
class SendgridSender implements NotificationSender {
async send(to: string, subject: string, body: string) { /* HTTP */ }
}
// Wire ở composition root:
const service = new OrderService(new PostgresOrderRepository(), new SendgridSender());
"Inversion" ở chỗ: thông thường high-level import low-level (high → low). Sau DIP, low-level implement abstraction định bởi high-level (low → high). Hướng phụ thuộc bị đảo ngược.
6.3. Test dễ hơn nhiều
// Mock đơn giản:
const fakeOrders: OrderRepository = {
save: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
};
const fakeMailer: NotificationSender = {
send: jest.fn(),
};
const service = new OrderService(fakeOrders, fakeMailer);
await service.checkout(order);
expect(fakeOrders.save).toHaveBeenCalledWith(order);
expect(fakeMailer.send).toHaveBeenCalled();
6.4. DI ≠ DIP
Đừng nhầm Dependency Inversion (nguyên lý) với Dependency Injection (kỹ thuật).
DI là 1 cách thực thi DIP — pass dependency vào qua constructor/setter thay vì new nội bộ.
Có DIP mà không DI cũng được (qua factory chẳng hạn).
7. Ngoài SOLID — các nguyên lý hữu ích khác
7.1. DRY — Don't Repeat Yourself
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." — Andy Hunt & Dave Thomas
Nhưng cẩn thận: "premature abstraction" còn tệ hơn duplicate. 3 dòng giống nhau ≠ phải DRY ngay. Đợi đến khi pattern rõ.
7.2. KISS — Keep It Simple, Stupid
Code đơn giản đánh bại code "thông minh". Nếu cần comment dài để giải thích → có thể đơn giản hóa.
7.3. YAGNI — You Aren't Gonna Need It
Đừng viết code cho yêu cầu tưởng tượng. Code cho hôm nay, refactor khi requirement mới đến.
7.4. Tell, Don't Ask
Thay vì hỏi object cho data rồi tự xử lý, bảo object làm việc đó:
// ❌ Ask:
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
// ✓ Tell:
account.withdraw(amount); // Account tự biết cần check
7.5. Law of Demeter — "Principle of Least Knowledge"
Method M của class C chỉ nên gọi method của:
- Chính C
- Object được tạo ra trong M
- Object truyền vào M (parameter)
- Field của C
// ❌ Vi phạm — chuỗi gọi sâu (train wreck)
order.getCustomer().getAddress().getCountry().getCode();
// ✓ Encapsulate:
order.getCustomerCountryCode();
7.6. CCP — Common Closure Principle (package level)
Class trong cùng package nên thay đổi vì cùng lý do. Khi 1 module trong package đổi, các module khác cùng package nên đổi cùng.
7.7. SOC — Separation of Concerns
Tách code thành các "concern" riêng biệt: UI, business, persistence. Liên quan đến SRP nhưng ở mức module/layer.
8. Khi SOLID phản tác dụng
SOLID có thể dẫn đến over-engineering nếu áp dụng máy móc:
8.1. SRP quá đà → "Class explosion"
Mỗi function 1 class, mỗi field 1 class. 1000 file cho việc 100 file đủ. Code phân mảnh, khó tìm.
8.2. OCP cho biến thể chưa tới
Tạo abstract + 5 subclass cho... 1 implementation duy nhất. YAGNI nói: chờ biến thể thực sự xuất hiện.
8.3. DIP "interface hell"
Mọi class có 1 interface. Mỗi UserService phải có IUserService. Đa số interface chỉ có 1 implementation và không bao giờ có cái thứ 2. Lãng phí tinh thần.
Quy tắc: tạo interface khi:
- Có ≥ 2 implementation thực sự (vd: PostgresRepo + InMemoryRepo cho test).
- Plugin extension point có thật (vd: payment gateway 3 nhà cung cấp).
- Cần mock cho test mà concrete class khó mock.
8.4. Liệu có thể "vi phạm" SOLID?
Có. Một class có thể có "2 lý do thay đổi" và OK nếu chúng luôn đi cùng nhau. Một subclass có thể "vi phạm LSP nhẹ" và acceptable trong context cụ thể. SOLID là guideline, không phải compile error.
9. Bài tập
- Đoạn code sau vi phạm nguyên lý nào? Refactor:
class Report { constructor(public title: string, public data: any[]) {} generatePdf() { /* ... */ } generateHtml() { /* ... */ } saveToDb() { /* ... */ } emailToUser(email: string) { /* ... */ } uploadToS3() { /* ... */ } } - Class sau vi phạm OCP:
Refactor sang abstraction. Thêm Country mới mà không sửa code cũ.function calculateShipping(country: string, weight: number): number { if (country === 'VN') return weight * 10000; if (country === 'US') return weight * 50000; if (country === 'JP') return weight * 30000; // Mỗi country mới phải sửa hàm này return weight * 100000; } - Tìm vi phạm LSP trong:
class Stack<T> { push(x: T) {} pop(): T | undefined { return undefined; } } class ImmutableStack<T> extends Stack<T> { push(x: T) { throw new Error('Immutable!'); } pop() { throw new Error('Immutable!'); } } - Refactor để tuân ISP:
interface Vehicle { drive(): void; fly(): void; sail(): void; } class Car implements Vehicle { /* fly và sail throw */ } class Plane implements Vehicle { /* drive và sail throw */ } - Sửa class sau theo DIP:
class UserService { private logger = new ConsoleLogger(); private repo = new PostgresUserRepo(); async create(name: string) { this.logger.log(`Creating ${name}`); await this.repo.save({ name }); } } - Khi nào KHÔNG nên áp dụng DIP (tạo interface)? Cho 2 ví dụ thực tế.
10. Quiz
Quiz cuối Chương 2
SRP nói "1 class chỉ có 1 lý do để thay đổi". Hệ quả thực tế:
OCP đạt được chủ yếu qua:
Subclass Square của Rectangle vi phạm LSP vì:
setWidth(10); setHeight(5); area==50 fail với Square. Cách sửa: tách hierarchy với immutability.ISP khuyên:
"Inversion" trong DIP nghĩa là:
Dependency Injection vs Dependency Inversion:
"YAGNI" cảnh báo:
"Tell, Don't Ask" khuyên:
if(account.getBalance() >= x) account.setBalance(...) bằng account.withdraw(x). Logic ở trong class, không phải ngoài.Hoàn thành Chương 2. Tiếp theo: Chương 3 — Class Design Mastery →