Nguồn: https://dotnettutorials.net/lesson/single-responsibility-principle/
SRP là một trong năm nguyên lý SOLID trong thiết kế và phát triển phần mềm hướng đối tượng. Mục tiêu của nguyên lý này là giúp phần mềm trở nên dễ hiểu hơn, linh hoạt hơn và dễ bảo trì hơn.
🔸 SRP phát biểu rằng:
Một class chỉ nên có một lý do duy nhất để thay đổi, nghĩa là nó chỉ nên đảm nhận một trách nhiệm hay một vai trò duy nhất.
Vậy điều này có nghĩa là gì?
- Ta nên thiết kế phần mềm sao cho mọi thứ trong một class đều liên quan đến cùng một nhiệm vụ chính.
- Không có nghĩa là class chỉ được phép có một method, bạn có thể có nhiều method miễn là chúng phục vụ cùng một mục đích chính.
- Khi áp dụng SRP, các class sẽ trở nên:
- Nhỏ gọn hơn
- Sạch hơn
- Và đặc biệt là dễ bảo trì hơn
Trước khi đi sâu vào SRP, bạn cần hiểu rõ “Trách nhiệm (Responsibility)” là gì:
- Trách nhiệm ở đây chính là vai trò hoặc mục đích chính mà class đó phục vụ trong hệ thống.
- Mỗi class chỉ nên chịu trách nhiệm cho một phần cụ thể của yêu cầu nghiệp vụ, không nên ôm đồm nhiều thứ không liên quan.
Làm thế nào để áp dụng Nguyên lý SRP trong C#?
-
Xác định rõ các trách nhiệm (Identify Responsibilities):
Bước đầu tiên là xác định rõ những trách nhiệm mà class của bạn đang hoặc sẽ đảm nhiệm.
Các trách nhiệm này thường thuộc các nhóm như:
- Truy cập dữ liệu (data access)
- Kiểm tra hợp lệ (validation)
- Ghi log (logging)
- Cache dữ liệu (caching)
- Chuyển đổi dữ liệu (serialization)
- Đăng ký người dùng (user registration)
- Xác thực (authentication)
- ...
👉 Nếu bạn thấy một class xử lý nhiều nhiệm vụ như trên, thì đó là dấu hiệu vi phạm SRP.
-
Tạo các class riêng biệt (Create Separate Classes):
Sau khi xác định được các trách nhiệm, ta cần tách mỗi trách nhiệm thành một class riêng biệt.
Không nên gộp nhiều chức năng không liên quan vào cùng một class.
Ví dụ: Nếu một class vừa xử lý đăng ký người dùng, vừa gửi email, vừa ghi log → nên chia thành:
UserRegistrationService
EmailService
LoggingService
-
Tách biệt các mối quan tâm (Separate Concerns):
Cuối cùng, đảm bảo rằng mỗi class chỉ thực hiện một nhiệm vụ cụ thể duy nhất, tập trung vào "mối quan tâm" chính của nó.
Điều này giúp bạn:
- Dễ mở rộng
- Dễ bảo trì
- Dễ test
Ví dụ minh họa Nguyên lý SRP bằng C#:
Giả sử bạn cần thiết kế một lớp Invoice
(Hóa đơn)
- Lớp
Invoice
sẽ thực hiện việc tính toán các khoản tiền (tổng, thuế, giảm giá...) dựa trên dữ liệu của hóa đơn. - Tuy nhiên, lớp này không nên kiêm luôn các công việc như:
- Truy xuất dữ liệu từ cơ sở dữ liệu
- Hiển thị dữ liệu
- Gửi email
- Ghi log
- Xuất hóa đơn ra file hoặc in ấn
Nếu ta gộp tất cả các logic trên vào cùng một lớp Invoice
, thì lớp đó sẽ có nhiều trách nhiệm:
- Truy cập cơ sở dữ liệu (Data Access)
- Xử lý nghiệp vụ (Business Logic)
- Gửi thông báo/email
- Ghi log...
➡ Điều này vi phạm nguyên lý SRP.
Khi một class có quá nhiều trách nhiệm sẽ dẫn đến
-
Khó hiểu (Difficult to Understand):
Rất khó để đọc và nắm bắt mục đích của từng phần code trong lớp.
-
Khó test (Difficult to Test):
Nếu muốn test logic tính toán tiền, bạn phải khởi tạo cả phần gửi email, kết nối database…
-
Dễ bị trùng lặp logic với phần khác (Chance of Duplicating Logic):
Vì nhiều phần trong app cũng cần xử lý tương tự, bạn có thể lặp lại logic ở nơi khác thay vì tái sử dụng code một cách hiệu quả.
Giải pháp theo SRP:
Tách các trách nhiệm riêng biệt thành các lớp:
Responsibility (Trách nhiệm) | Class name (Tên class) |
---|---|
Xử lý tính toán hóa đơn | InvoiceCalculator |
Ghi log | Logger |
Gửi email | EmailSender |
Truy xuất dữ liệu | InvoiceRepository |
In hóa đơn | InvoicePrinter |
Ví dụ KHÔNG tuân theo SRP trong C#
Theo như hình trên, lớp Invoice
có 4 chức năng
Add Invoice
Delete Invoice
Sending Email
Error Logging
Thì trong đó 2 chức năng Add Invoice
và Delete Invoice
là 2 chức năng chính của lớp Invoice
.
Hai chức năng còn lại là Sending Email
và Error Logging
là 2 chức năng không nên có trong lớp Invoice
điều này làm lớp.
using System;
using System.Net.Mail;
namespace SOLID_PRINCIPLES.SRP
{ public class Invoice { public long InvoiceAmount { get; set; } public DateTime InvoiceDate { get; set; } public void AddInvoice() { try { // Here we need to write the Code for adding invoice // Once the Invoice has been added, then send the mail MailMessage mailMessage = new MailMessage("EMailFrom", "EMailTo", "EMailSubject", "EMailBody"); this.SendInvoiceEmail(mailMessage); } catch (Exception ex) { //Error Logging System.IO.File.WriteAllText(@"c:\ErrorLog.txt", ex.ToString()); } } public void DeleteInvoice() { try { //Here we need to write the Code for Deleting the already generated invoice } catch (Exception ex) { //Error Logging System.IO.File.WriteAllText(@"c:\ErrorLog.txt", ex.ToString()); } } public void SendInvoiceEmail(MailMessage mailMessage) { try { // Here we need to write the Code for Email setting and sending the invoice mail } catch (Exception ex) { //Error Logging System.IO.File.WriteAllText(@"c:\ErrorLog.txt", ex.ToString()); } } } }
Với thiết kế này, nếu chúng ta muốn thay đổi chức năng ghi log hoặc chức năng gửi email, thì chúng ta buộc phải chỉnh sửa lớp Invoice
. Điều này vi phạm Single Responsibility Principle, bởi vì chúng ta đang thay đổi lớp Invoice
để phục vụ cho các chức năng không thuộc trách nhiệm chính của nó.
Khi thực hiện những thay đổi như vậy, chúng ta cần phải kiểm thử lại toàn bộ các chức năng, bao gồm: chức năng ghi log, gửi email và xử lý hóa đơn. Điều này làm cho quá trình phát triển và bảo trì trở nên khó khăn, mất thời gian và dễ phát sinh lỗi.
Ví dụ CÓ tuân theo SRP trong C#
Theo như hình trên
- Lớp
Invoice
sẽ thực thi các chức năng liên quan đến nó. - Lớp
Logger
sẽ chỉ sử dụng với mục đích logging. - Lớp
Email
sẽ xử lí các hoạt động liên quan tới mail
→ Vậy giờ mỗi lớp có các “trách nhiệm” đúng với chính nó.
Với thiết kế đã cải tiến như trên, nếu bạn muốn thay đổi chức năng gửi email, bạn chỉ cần sửa đổi lớp Email
, không cần đụng đến các lớp Invoice
hay Logging
. Tương tự, nếu bạn muốn chỉnh sửa chức năng xử lý hóa đơn (Invoice
), bạn chỉ cần thay đổi lớp Invoice
, không ảnh hưởng đến lớp Email
hay Logging
.
Điều này đúng với nguyên lý SRP (Single Responsibility Principle) – mỗi lớp chỉ chịu trách nhiệm về một vai trò duy nhất, nên việc bảo trì, kiểm thử và mở rộng hệ thống trở nên dễ dàng và an toàn hơn.
Logger.cs
Ở đây, chúng ta tạo một Interface có tên là ILogger
với ba phương thức trừu tượng (abstract) (mặc định, các phương thức trong interface đều là trừu tượng).
Sau đó, chúng ta cài đặt các phương thức của interface ILogger
trong lớp Logger
.
Các phương thức Info
, Debug
và Error
sẽ thực hiện các hoạt động ghi log khác nhau, và tất cả các phương thức này đều được đặt trong lớp Logger
.
using System;
namespace SOLID_PRINCIPLES.SRP
{ public interface ILogger { void Info(string info); void Debug(string info); void Error(string message, Exception ex); } public class Logger : ILogger { public Logger() { // here we need to write the Code for initialization // that is Creating the Log file with necesssary details } public void Info(string info) { // here we need to write the Code for info information into the ErrorLog text file } public void Debug(string info) { // here we need to write the Code for Debug information into the ErrorLog text file } public void Error(string message, Exception ex) { // here we need to write the Code for Error information into the ErrorLog text file } }
}
MailSender.cs
namespace SOLID_PRINCIPLES.SRP
{ public class MailSender { public string EMailFrom { get; set; } public string EMailTo { get; set; } public string EMailSubject { get; set; } public string EMailBody { get; set; } public void SendEmail() { // Here we need to write the Code for sending the mail } }
}
Invoice.cs
using System.Net.Mail;
using System;
namespace SOLID_PRINCIPLES.SRP
{ public class Invoice { public long InvAmount { get; set; } public DateTime InvDate { get; set; } private ILogger fileLogger; private MailSender emailSender; public Invoice() { fileLogger = new Logger(); emailSender = new MailSender(); } public void AddInvoice() { try { fileLogger.Info("Add method Start"); // Here we need to write the Code for adding invoice // Once the Invoice has been added, then send the mail emailSender.EMailFrom = "emailfrom@xyz.com"; emailSender.EMailTo = "emailto@xyz.com"; emailSender.EMailSubject = "Single Responsibility Princile"; emailSender.EMailBody = "A class should have only one reason to change"; emailSender.SendEmail(); } catch (Exception ex) { fileLogger.Error("Error Occurred while Generating Invoice", ex); } } public void DeleteInvoice() { try { //Here we need to write the Code for Deleting the already generated invoice fileLogger.Info("Delete Invoice Start at @" + DateTime.Now); } catch (Exception ex) { fileLogger.Error("Error Occurred while Deleting Invoice", ex); } } }
}
Chúng ta cần thiết kế ứng dụng theo đúng Single Responsibility Principle - SRP trong C#.
Điều này có nghĩa là mỗi lớp trong ứng dụng phải có một trách nhiệm duy nhất của riêng nó, và chỉ nên có một lý do duy nhất khiến lớp đó cần được thay đổi.
Nói cách khác, mỗi module phần mềm hoặc mỗi lớp nên chỉ tập trung vào một chức năng cụ thể, không đảm nhiệm quá nhiều vai trò khác nhau.
Lợi ích của việc áp dụng Nguyên lý SRP trong C#:
-
Dễ hiểu hơn:
Các lớp chỉ đảm nhiệm một trách nhiệm duy nhất thường nhỏ gọn và tập trung hơn, giúp chúng dễ đọc và dễ hiểu. Mỗi lớp có mục đích rõ ràng, nên lập trình viên có thể nhanh chóng nắm bắt được chức năng của nó.
-
Dễ sửa đổi hơn:
Khi các lớp được thiết kế chỉ với một trách nhiệm, thì những thay đổi trong yêu cầu của hệ thống sẽ chỉ ảnh hưởng đến ít thành phần hơn. Điều này giúp việc cập nhật và bảo trì mã nguồn trở nên dễ dàng, vì thay đổi ở một phần của hệ thống sẽ ít có khả năng ảnh hưởng đến các phần khác.
-
Dễ kiểm thử hơn:
SRP tạo ra các lớp nhỏ gọn hơn, do đó việc kiểm thử cũng đơn giản hơn. Mỗi bài test có thể tập trung kiểm tra một chức năng duy nhất, giúp giảm độ phức tạp của test case. Ngoài ra, việc viết và hiểu các unit test cũng dễ dàng hơn.
-
Tăng khả năng tái sử dụng:
Những lớp chỉ phụ trách một chức năng cụ thể thì có khả năng tái sử dụng cao hơn trong các phần khác của ứng dụng — hoặc thậm chí trong các dự án khác.
-
Tổ chức mã nguồn tốt hơn:
SRP giúp tổ chức codebase một cách khoa học hơn. Mỗi lớp và mỗi module sẽ tập trung vào một phần riêng biệt của ứng dụng, điều này rất quan trọng trong phát triển hiện đại, ví dụ như mô hình Microservices, nơi mỗi dịch vụ đều tập trung vào một chức năng nghiệp vụ cụ thể.