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:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
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.
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é).
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"); } }
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"); } }
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(); }
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 { }
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.
(toidicodedao)
No comments:
Post a Comment