← SOLID Principles

Bài 2 · Vận dụng · 20 phút· Cập nhật 11/06/2026

S - Single Responsibility

Biên soạn bởi Nguyễn Anh Tuấn

Single Responsibility Principle (SRP): nguyên tắc đơn nhiệm, mỗi lớp chỉ chịu trách nhiệm về một và chỉ một nhiệm vụ cụ thể, dễ test và bảo trì.

Single Responsibility Principle (SRP): mỗi lớp chỉ nên chịu trách nhiệm về một việc duy nhất - hay nói cách khác, chỉ có một lý do để thay đổi. Hãy xem một lớp vi phạm:

❌ Vi phạm SRP - OrderProcessor làm hết

class OrderProcessor {
  checkout(order: Order) {
    // (1) Xu ly thanh toan
    const ok = chargeCard(order.card, order.total);
    if (!ok) throw new Error("Thanh toan that bai");

    // (2) Xu ly van chuyen
    const label = createShippingLabel(order.address);
    bookCourier(label);

    // (3) Gui email xac nhan
    sendEmail(order.email, "Don hang da xac nhan");
  }
}
  • Một lớp gánh BA trách nhiệm: thanh toán, vận chuyển, thông báo.
  • Ba "tác nhân" khác nhau (cổng thanh toán, hãng ship, hệ thống mail) đều có thể buộc sửa lớp này.
  • Đổi nhà cung cấp email cũng phải mở đúng lớp xử lý thanh toán - rủi ro gãy chéo.

Tách mỗi trách nhiệm thành một lớp riêng, rồi để một lớp điều phối ghép chúng lại:

✅ Tuân thủ SRP - tách trách nhiệm

class PaymentProcessor {
  pay(order: Order): void {
    const ok = chargeCard(order.card, order.total);
    if (!ok) throw new Error("Thanh toan that bai");
  }
}

class ShippingProcessor {
  ship(order: Order): void {
    const label = createShippingLabel(order.address);
    bookCourier(label);
  }
}

class OrderNotifier {
  confirm(order: Order): void {
    sendEmail(order.email, "Don hang da xac nhan");
  }
}

// Lop dieu phoi: chi GHEP cac buoc, khong tu lam chi tiet
class OrderProcessor {
  constructor(
    private payment: PaymentProcessor,
    private shipping: ShippingProcessor,
    private notifier: OrderNotifier,
  ) {}

  checkout(order: Order): void {
    this.payment.pay(order);
    this.shipping.ship(order);
    this.notifier.confirm(order);
  }
}
  • Mỗi lớp giờ chỉ có MỘT lý do để thay đổi.
  • Đổi cổng thanh toán → chỉ sửa PaymentProcessor; phần còn lại không đụng tới.
  • OrderProcessor trở thành "nhạc trưởng" - chỉ điều phối, không ôm chi tiết.

Tách trách nhiệm mở đường cho DIP

Để ý OrderProcessor nhận các lớp con qua constructor thay vì tự new. Đó là mầm mống của Dependency Inversion (Bài 6) - SRP tách việc xong thì việc "lắp ráp" trở nên tự nhiên.

Lớp một trách nhiệm thì test gọn - không phải dựng cả luồng đặt hàng để kiểm thanh toán:

payment.test.ts - test độc lập

import { describe, it, expect, vi } from "vitest";
import { chargeCard } from "./payment-gateway";

vi.mock("./payment-gateway");   // mock module truoc, vi.mocked moi co tac dung

describe("PaymentProcessor", () => {
  it("nem loi khi the bi tu choi", () => {
    vi.mocked(chargeCard).mockReturnValue(false);
    const order = { id: 1, total: 499000 };
    expect(() => new PaymentProcessor().pay(order))
      .toThrow("Thanh toan that bai");
  });
});
  • Test PaymentProcessor không cần email hay vận chuyển.
  • Ít phụ thuộc → ít mock → test nhanh, rõ ý.
  • Lỗi ở đâu lộ ra ở đó: test đỏ chỉ thẳng vào trách nhiệm bị hỏng.
  • Dấu hiệu vi phạm: tên lớp có "And" hoặc "Manager/Handler" mơ hồ (Processor kèm phạm vi hẹp như PaymentProcessor thì ổn); method dài; nhiều import không liên quan.
  • Hỏi "lớp này thay đổi vì ai/vì điều gì?" - nếu trả lời được nhiều thứ, hãy tách.
  • Đừng tách tới mức vô nghĩa: SRP là về LÝ DO thay đổi, không phải "mỗi lớp một dòng".

Một lý do để thay đổi

Cách kiểm tra nhanh: hình dung các phòng ban (tác nhân) trong công ty. Nếu hai phòng khác nhau có thể yêu cầu sửa cùng một lớp vì hai lý do khác nhau → lớp đó đang gánh hai trách nhiệm.

Câu hỏi thường gặp

Uncle Bob diễn đạt lại SRP là: "một lớp chỉ nên có MỘT lý do để thay đổi", và mỗi lý do thường gắn với một TÁC NHÂN (actor) - một nhóm người/bộ phận yêu cầu thay đổi. Nếu phòng Kế toán và phòng Vận hành đều có thể khiến bạn phải sửa cùng một lớp, lớp đó đang gánh hai trách nhiệm.

Không. SRP nói về một LÝ DO ĐỂ THAY ĐỔI, không phải một method. Một lớp có thể có nhiều method miễn chúng cùng phục vụ một trách nhiệm gắn kết (cohesive). Tách quá nhỏ (mỗi lớp một method vô nghĩa) lại là một thái cực sai khác.

Số file tăng là đánh đổi có thật. Nhưng mỗi file ngắn, tên rõ, dễ tìm và dễ test hơn một "God class" 800 dòng. Đặt tên & tổ chức thư mục tốt sẽ bù lại. Với module rất nhỏ và ổn định, đừng tách quá tay.

Cân bằng để không lạm dụng: bài Áp dụng SOLID tổng hợp →

Rất nhiều. Lớp một trách nhiệm thì ít phụ thuộc, đầu vào/đầu ra rõ → viết unit test dễ, ít phải mock. Khi PaymentProcessor tách khỏi email & vận chuyển, bạn test logic thanh toán mà không cần dựng cả luồng đặt hàng.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/5 Điểm: 0

Single Responsibility Principle phát biểu rằng một lớp nên…

Bài tập về nhà

  1. 1

    Đếm "lý do để thay đổi"

    Lấy lớp OrderProcessor "bẩn" ở Bước 1. Liệt kê từng lý do khiến nó phải sửa, và gắn mỗi lý do với một tác nhân (Kế toán, Vận hành, Marketing…).

    ✅ Hoàn thành khi: Liệt kê ≥3 lý do, mỗi lý do gắn đúng một tác nhân khác nhau.

  2. 2

    Tách trách nhiệm

    Refactor một lớp ôm đồm trong code của mèo con (hoặc lớp Report: truy vấn + tính + dựng HTML) thành các lớp nhỏ theo SRP.

    ✅ Hoàn thành khi: Mỗi lớp mới có một trách nhiệm rõ; lớp điều phối chỉ ghép chúng lại, không tự làm hết.

  3. 3

    Test từng phần

    Viết unit test cho MỘT lớp đã tách (vd PaymentProcessor) mà không cần dựng các phần khác.

    ✅ Hoàn thành khi: Test chạy độc lập, không phụ thuộc email/vận chuyển; cover ít nhất một ca thành công + một ca lỗi.

  4. 4

    Đừng tách quá tay

    Tìm một ví dụ tách nhỏ tới mức gây rối (vd mỗi getter một lớp). Lập luận vì sao nó vi phạm tinh thần thay vì phục vụ SRP.

    ✅ Hoàn thành khi: Nêu được rằng SRP là về "lý do thay đổi", không phải "càng nhỏ càng tốt".