Series SOLID cho thanh niên code CỨNG: Single Responsibility Principle

Cách đây khá lâu, mình đã có một bài viết tổng quát về SOLID Principle, những nguyên lý thiết kế OOP. Nhắc lại một chút cho các bạn đã quên.

Đây là những nguyên lý được đúc kết bởi máu xương vô số developer, rút ra từ hàng ngàn dự án thành công và thất bại. Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng hơn. Và việc quan trọng nhất là việc maintainace code sẽ dễ hơn rất nhiều.

Nắm vững những nguyên lý này, đồng thời áp dụng chúng trong việc thiết kế + viết code sẽ giúp bạn tiến thêm 1 bước trên con đường thành senior nhé (1 ông senior bên FPT Software từng bảo mình thế).

SOLID bao gồm 5 nguyên lý dưới đây:

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

keepcalm

Giới thiệu

Đây là bài viết đầu tiên trong series “SOLID cho thanh niên cứng”. Các nguyên lý SOLID này khá hữu ích, nhưng mình không thấy được dạy ở các trường. Ở mỗi bài viết, mình sẽ phân tích rõ hơn về các nguyên lý này, kèm theo code minh họa. Hi vọng chúng sẽ giúp các bạn hiểu rõ hơn và áp dụng nguyên lý này vào code.

Ở bài viết đầu tiên, mình sẽ nói về Single Responsibility Principle – Nguyên lý Đơn Trách Nhiệm. Nội dung nguyên lý:

Một class chỉ nên giữ một trách nhiệm duy nhất 
(Chỉ có thể thay đổi class vì một lý do duy nhất)

Giải thích nguyên lý

Ta có thể tạm hiểu “trách nhiệm” ở đây tương đương với “chức năng”. Tại sao một class chỉ nên giữ một chức năng duy nhất?? Để hiểu điều này, hãy nhìn vào hình dưới.

SingleResponsibility

Hãy xem con dao trong hình như một class với rất nhiều chức năng. Con dao này “có vẻ” khá là tiện dụng, nhưng lại cồng kềnh và nặng nề. Đặc biệt, khi có một bộ phận bị hư hỏng, ta phải tháo nguyên con dao ra để sửa. Việc sửa chữa và cải tiến rất phức tạp, có thể ảnh hưởng tới nhiều bộ phận khác nhau.

Một class có quá nhiều chức năng cũng sẽ trở nên cồng kềnh và phức tạp. Trong ngành IT, requirement rất hay thay đổi, dẫn tới sự thay đổi code. Nếu một class có quá nhiều chức năng, quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác.

Áp dụng SRP vào con dao phía trên, ta có thể tách nó ra làm kéo, dao, mở nút chai,… riêng biệt là xong, cái gì hư chỉ cần sửa cái đấy. Với code cũng vậy, ta chỉ cần thiết kế các module sao cho đơn giản, một module chỉ có 1 chức năng duy nhất là xong (Nói vậy chứ việc xác định, gom nhóm chức năng không hề dễ đâu nhé).

HighLevelDesign

Ví dụ minh họa

Đoạn code dưới đây là ví dụ cho việc vi phạm SRP. Lỗi này hồi mới học code mình cũng hay mắc phải. Class Student có quá nhiều chức năng: chứa thông tin học sinh, format hiển thị thông tin, lưu trữ thông tin.


public class Student {
public string Name { get; set;}
public int Age { get; set;}
// Format class này dưới dạng text, html, json để in ra
public string GetStudentInfoText() {
return "Name: " + Name + ". Age: " + Age;
}
public string GetStudentInfoHTML() {
return "<span>" + Name + " " + Age + "</span>";
}
public string GetStudentInfoJson() {
return Json.Serialize(this);
}
// Lưu trữ xuống database, xuống file
public void SaveToDatabase() {
dbContext.Save(this);
}
public void SaveToFile() {
Files.Save(this, "fileName.txt");
}
}

view raw

srpBefore.cs

hosted with ❤ by GitHub

Code như thế thì có làm sao không? Hiện tại thì không sao cả, nhưng khi code lớn dần, thêm chức năng nhiều hơn, class Student sẽ bị phình to ra. Chưa kể, nếu như có thêm các class khác như Person, Teacher v…v, đoạn code hiển thị/lưu trữ thông tin sẽ nằm rải rác ở nhiều class, rất khó sửa chữa và nâng cấp.

Để giải quyết, ta chỉ cần tách ra làm nhiều class, mỗi class có một chức năng riêng là xong. Khi cần nâng cấp, sửa chữa, sẽ diễn ra ở từng class, không ảnh hưởng tới các class còn lại.


// Student bây giờ chỉ chứa thông tin
public class Student {
public string Name { get; set;}
public int Age { get; set;}
}
// Class này chỉ format thông tin hiển thị student
public class Formatter {
public string FormatStudentText(Student std) {
return "Name: " + std.Name + ". Age: " + std.Age;
}
public string FormatStudentHtml(Student std) {
return "<span>" + std.Name + " " + std.Age + "</span>";
}
public string FormatStudentJson(Student std) {
return Json.Serialize(std);
}
}
// Class này chỉ lo việc lưu trữ
public class Store {
public void SaveToDatabase(Student std) {
dbContext.Save(std);
}
public void SaveToFile(Student std) {
Files.Save(std, "fileName.txt");
}
}

view raw

srpAfter.cs

hosted with ❤ by GitHub

Lưu ý: Không phải lúc nào cũng nên áp dụng nguyên lý này vào code. Một trường hợp hay gặp là các class dạng Helper hay Utilities – các class này vi phạm SRP 1 cách trắng trợn. Nếu số lượng hàm ít, ta vẫn có thể cho tất cả các hàm này vào 1 class, xét cho cùng, toàn bộ các hàm trong helper đều có chức năng xử lý các tác vụ nho nhỏ.


// Class Helper vi phạm SRP
// Nhưng vì nhỏ, ta có thể bỏ qua
public class Helper {
public string GetUser();
public DateTime GetTime();
public string GetCurrentLocation();
public DbConnection GetDatabaseConnection();
}

view raw

helperSmall.cs

hosted with ❤ by GitHub

Tuy nhiên, khi Helper có thêm nhiều chức năng, nó trở nên phức tạp và cồng kềnh hơn (Bạn mình từng gặp trường hợp một class có tới gần 10000 dòng code). Lúc này, ta cần áp dụng SRP để chia nó thành các module nho nhỏ để dễ quản lý.


// Helper đã bự, ta cần tách
public class Helper {
public string GetUser();
//…..
public DateTime GetTime();
//…..
public string GetCurrentLocation();
//…..
public DbConnection GetDatabaseConnection();
}
// Tách helper thành các helper nhỏ hơn
public class UserHelper {
}
public class TimeLocationHelper {
}
public class DatabaseHelper {
}

view raw

helperBig.cs

hosted with ❤ by GitHub

Lưu ý và kết luận

Về bản chất, nguyên lý chỉ là nguyên lý, nó chỉ là hướng dẫn chứ không phải là quy tắc tuyệt đối bất di bất dịch. Nếu tìm hiểu kĩ, các bạn sẽ thấy vẫn có vài lập trình viên mổ xẻ, phản đối, chỉ ra những chỗ chưa ổn của các nguyên lý này. Tuy vậy, việc hiểu rõ chúng vẫn giúp code ta viết ra dễ đọc, dễ hiểu, dễ quản lý hơn.

SRP là nguyên lý đơn giản dễ hiểu nhất, nhưng cũng khó áp dụng đúng nhất. Sự khác nhau giữa dev giỏi và dev bình thường là ở chỗ, cả 2 cùng biết về các qui tắc và nguyên lý, nhưng dev giỏi sẽ biết khi nào cần áp dụng, khi nào không.

photo-4-1

Những nguyên tắc còn lại của SOLID sẽ được giới thiệu ở những bài viết sau nhé. Có thắc mắc hay góp ý gì các bạn cứ thoải mái nêu ra trong phần comment, mình sẽ cố gắng giải đáp.

Một số tài liệu để tham khảo thêm:

10 thoughts on “Series SOLID cho thanh niên code CỨNG: Single Responsibility Principle”

  1. đúng là SOLID này ở Trường không dạy thật. Nhưng kiểu gom tất cả các chức năng của 1 đối tượng vào 1 lớp lại được giảng dạy phổ biến ở Trường.
    P/s: đúng là học và làm cực kỳ khác nhau 😀

    Like

  2. Mình muốn hỏi thêm 1 vấn đề là
    Có nên apply nguyên lý SRP cho phương thức của Class không nhỉ? Hay chỉ cần apply cho Class là đúng và đủ với nguyên lý rồi.

    Like

    1. Một số nguyên tắc khi viết phương thức mình biết được khi học ở trường
      1. Độ dài các phương thức không nên dài quá 20 dòng hoặc 1 trang A4
      2. Phương thức nên chỉ thể hiện 1 nhiệm vụ, k nên giải quyết nhiều vấn đề trong 1 method –> Khó tái sử dụng, khó bảo trì

      Liked by 1 person

  3. Các Nguyên lý này được bách khoa dạy khá kỹ nhé. Mà công nhận tác giả hiểu kỹ vấn đề

    Like

Leave a comment