Single Responsibility: một class, một lý do để thay đổi
Single Responsibility Principle nói một module/class nên có một lý do để thay đổi. "Lý do" ở đây gắn với một nhóm người dùng hoặc một mối quan tâm: logic nghiệp vụ, định dạng dữ liệu, lưu trữ, gửi thông báo. Một class trộn nhiều mối quan tâm sẽ bị kéo sửa bởi nhiều lý do không liên quan, mỗi lần sửa lại có nguy cơ làm hỏng phần khác. SRP không phải "class càng nhỏ càng tốt" — mà là gom những thứ thay đổi cùng nhau, tách những thứ thay đổi vì lý do khác nhau.
Cơ chế hoạt động
Xét một OrderService ôm tất cả:
class OrderService {
placeOrder(data) {
if (!data.items.length) throw new Error('rỗng') // validation
const total = data.items.reduce(...) // tính tiền (nghiệp vụ)
db.query('insert into orders ...') // lưu trữ
smtp.send(data.email, renderHtml(order)) // gửi email + render template
}
}
Class này thay đổi vì: quy tắc validation đổi, công thức tính tiền đổi, schema DB đổi, nội dung email đổi, cách gửi mail đổi. Năm lý do, năm nhóm người khác nhau yêu cầu. Tách theo trách nhiệm:
class OrderService {
constructor(
private validator: OrderValidator,
private pricing: PricingService,
private repo: OrderRepository,
private notifier: OrderNotifier,
) {}
placeOrder(data) {
this.validator.validate(data)
const order = this.pricing.build(data)
this.repo.save(order)
this.notifier.sendConfirmation(order)
}
}
Giờ OrderService điều phối; mỗi mối quan tâm sống trong đơn vị riêng thay đổi độc lập.
Vấn đề gặp trong production
Failure mode: sửa một thứ làm hỏng thứ khác. Trong class ôm tất cả, đổi template email có thể vô tình động tới logic lưu DB (cùng method, cùng state), và một thay đổi nhỏ buộc test lại toàn bộ. Bug "sửa A hỏng B" mà A và B chẳng liên quan gì là dấu hiệu kinh điển của vi phạm SRP. Tách ra thì thay đổi được khoanh vùng: đổi email chỉ đụng OrderNotifier.
Failure mode: không test được phần nghiệp vụ tách khỏi I/O. Class trộn tính tiền với gọi DB và SMTP thì để test công thức tính tiền phải mock cả database lẫn mail server — test chậm, giòn, khó viết. Tách PricingService thuần (không I/O) cho phép test logic tiền bằng unit test nhanh, không mock gì.
Failure mode: phản ứng quá đà — quá nhiều class tí hon. Áp SRP máy móc tới mức mỗi hàm một class, mỗi thao tác một interface, tạo ra hàng chục file phải nhảy qua lại để hiểu một luồng đơn giản. Đây là cái giá thật của SRP làm quá. "Một trách nhiệm" là một mối quan tâm mạch lạc, không phải "một dòng code". Gộp những thứ luôn thay đổi cùng nhau; chỉ tách khi chúng thật sự có lý do thay đổi riêng.
Cách debug và monitor
Dấu hiệu một class vi phạm SRP: tên mơ hồ kiểu Manager/Helper/Util, độ dài lớn, nhiều dependency không liên quan nhau (DB + mail + HTTP trong một class), và lịch sử git cho thấy nó bị sửa bởi nhiều loại thay đổi khác nhau. Một heuristic mạnh: nhìn lịch sử commit của file — nếu cùng một file bị đụng vì "đổi giá", "đổi email", "đổi schema" thì nó đang ôm nhiều trách nhiệm. Khi test một logic phải mock nhiều hạ tầng không liên quan, đó cũng là tín hiệu tách. Ngược lại, nếu phải sửa năm file mỗi khi đổi một quy tắc, có thể đã tách quá đà.
Tradeoff
Tách trách nhiệm cho code dễ thay đổi an toàn (tác động khoanh vùng), dễ test (logic thuần tách khỏi I/O), dễ hiểu từng phần. Cái giá là nhiều đơn vị hơn, nhiều file hơn, và chi phí điều phối/đi qua lại giữa chúng. Tách quá tay làm một luồng đơn giản trải khắp chục file, khó theo dõi. Quy tắc thực tế: tách theo lý do thay đổi (mối quan tâm, nhóm người dùng), không theo số dòng; gom thứ thay đổi cùng nhau; dùng lịch sử thay đổi thật làm bằng chứng cho ranh giới. SRP là cân bằng giữa "một khối hỗn độn" và "vụn thành cát", không phải cực đoan nào.
Câu hỏi phỏng vấn
SRP là gì, và "một trách nhiệm" được đo bằng gì?
SRP nói một module/class nên có một lý do để thay đổi — tức gắn với một mối quan tâm hoặc một nhóm người dùng duy nhất (logic nghiệp vụ, định dạng, lưu trữ, thông báo). "Một trách nhiệm" không đo bằng số dòng hay số method mà bằng lý do thay đổi: nếu một class bị kéo sửa bởi nhiều loại yêu cầu không liên quan (đổi công thức giá, đổi template email, đổi schema DB) thì nó đang ôm nhiều trách nhiệm và cần tách. Tách giúp khoanh vùng tác động (sửa email không đụng lưu trữ) và tách logic thuần khỏi I/O để test nhanh không cần mock hạ tầng. Điểm ăn điểm: cảnh báo tách quá đà tạo hàng chục class tí hon làm một luồng đơn giản trải khắp nhiều file; dùng lịch sử git (file bị sửa vì những lý do gì) làm bằng chứng thực cho ranh giới trách nhiệm.
Hands-on
Lấy một service "ôm tất cả" thật (kiểu OrderService.placeOrder làm validation + tính tiền + lưu DB + gửi email trong một method), liệt kê các lý do thay đổi khác nhau của nó, rồi tách thành validator, pricing (thuần, không I/O), repository, và notifier, để service gốc chỉ điều phối. Viết unit test cho phần pricing mà không mock DB/SMTP để thấy nó test được độc lập. Sau đó nhìn lịch sử git của file gốc (nếu có) để xác nhận nó từng bị sửa bởi nhiều loại thay đổi, và đối chiếu: sau khi tách, mỗi loại thay đổi giờ chỉ đụng một file.
Top comments (0)