SOLID là thay đổi
Trong bài trước SOLID - Tổng quan, tôi đưa ra quan điểm rằng SOLID không nhằm tạo ra những dòng code bất tử mà nhằm tạo ra những dòng code sẵn sàng thay đổi. Có thể cái tên SOLID khiến ta có chút liên tưởng ngược lại, nhưng từng nguyên tắc trong đó lại toàn nói về sự thay đổi.
Single Responsibility - không có nhiều hơn một lý do để thay đổi class
Ngay cái định nghĩa đã có từ thay đổi. Bạn viết một class và bạn phải đặt ra câu hỏi "NẾU CẦN THAY ĐỔI THÌ LÀM SAO". Hãy chuẩn bị cho một tương lai không chắc chắn. Viết ra mọi thứ đơn nhiệm và bạn sẽ tối thiếu hóa xung đột sau này khi những yêu cầu sửa đổi, mở rộng được thêm vào. Việc sửa đổi ở class A sẽ không biến thành một vụ rút dây động rừng. Sửa ở A không kéo theo việc sửa ở B vì A và B là những class đơn nhiệm. Bạn không viết ra 2 class để làm cùng 1 nhiệm vụ đúng không? Nguyên tắc đầu này nhắc nhở bạn hãy luôn sẵn sàng cho việc thay đổi.
Open/Closed: thiết kế để sẵn sàng mở rộng chứ không sửa đổi
Mở rộng hay sửa đổi thì cũng đều là thay đổi cả. Nguyên tắc này hướng dẫn bạn cách thay đổi cho đúng. Tôi nhét từ thiết kế vào bởi vì, "mở rộng không sửa đổi" không phải chỉ là quy tắc làm việc. Cố gắng không thay đổi những gì đã chạy là tốt, nhưng thiết kế mã nguồn không cho phép mở rộng thì bạn cũng chẳng làm gì được. Khi chắp bút cho một chức năng, hãy chuẩn bị luôn con đường để thay đổi nó trong tương lai, hãy thiết kế nó.
Liskov substitution: con phải kế tục được cha
Đầy sự thay đổi trong nguyên tắc này. Một thực tế trong lập trình, các bạn sẽ thực thi những lớp con cháu chứ không phải lớp cha. Nhưng khi khai báo lớp cha, các bạn vốn không biết trước các lớp con cháu sẽ như thế nào. Thực ra các bạn không quan tâm các lớp con cháu sẽ như thế nào. Nó có gen của lớp cha là được. Viết code để sự thay đổi là an toàn.
Interface segregation: tôi sẽ không phải phụ thuộc vào những hành vi mà tôi không sử dụng
Nguyên tắc này cũng hướng dẫn chúng ta đến sự thay đổi an toàn. Việc gì xảy ra khi tôi phải phụ thuộc vào thứ không liên quan tới tôi? Có nghĩa là một ngày đẹp trời tôi ngập trong rắc rối mặc dù tôi chẳng làm gì cả.
Dependency Inversion: không phụ thuộc vào code thực thi, mà là các khai báo trừu tượng
Bạn sẽ toát mồ hôi hột khi thay đổi một phụ thuộc cứng. Điều tồi tệ là dù có làm được, bạn cũng không chắc chắn về kết quả, công sức bỏ ra thì nhiều khi bạn phải code lại kha khá. Việc trừu tượng hóa vấn đề giống như bạn mua bảo hiểm vậy, vô dụng cho hiện tại, nhưng là cái phao cứu sinh khi rủi ro và là của để dành sau này.
Lời thú tội ở đây: tôi chỉ đang cố gắng thuyết phục, nhồi nhét vào đầu các bạn 2 chữ thay đổi. Viết code không phải là xây dựng những tượng đài trường tồn với thời gian. Mã nguồn SOLID hóa ra lại phải LIQUID. À thực ra là những nguyên tắc SOLID để có mã nguồn LIQUID. Những nguyên tắc này xoay quanh những mệnh đề: Nó sẽ thay đổi - Tại sao lại thay đổi - Thay đổi thành cái gì - Thay đổi như thế nào (thời trang hơn thì là thế này: WHEN it changes - WHY it change - WHAT it change to be - HOW it change). Nếu bạn có hai chữ "thay đổi" trong não, SOLID sẽ đến với bạn chứ không phải bạn chạy theo SOLID.
Các bài tiếp theo, chúng ta sẽ bàn bạc từng nguyên tắc một.
All rights reserved
Bình luận
Hi, chào @refacore , mình có một chút câu hỏi liên quan đến chủ đề này
Liệu Factory Method Design Pattern có phải là một cách triển khai nguyên lý Dependency Inversion (High-level modules should not depend on the low-level modules, both should depend on the abstraction)
Interface Database { String connectToDataBase() ; }
MongoDb implement Database { String connectToDataBase(){
return "this is MongoDb"; } }
Mysql implement Database { String connectToDataBase(){
return "this is Mysql "; } }
// trên đây chúng ta cũng có thể thấy MongoDb và Mysql (High-level đang phụ thuộc vào thằng Interface Database (abstraction) )
Class DatabaseFactory {
public Database createConnection(string type) {
if(type.equa("MongoDb")){ return new MongoDb ();
} else if(type.equa("Mysql")){ return new Mysql(); }
}
// nếu mình muốn có một loại database khác thì cũng chỉ việc tạo một loại database khác và extends Database và else if(type.equa("***")){return....
Cám ơn bạn nhiều
DI là một nguyên tắc về thiết kế, nên nó xuất hiện trong toàn bộ code, khác với Design Pattern, được thiết kế cho một trường hợp riêng biệt. Các Design Pattern hầu hết đều tuân theo DI.
Việc một class triển khai một interface còn có lý do khác đằng sau liên quan đến unit test. Interface cho phép lập trình viên dễ dàng mock các dependency, qua đó cô lập class cần test để viết unit test dễ dàng. Các class tiện tích thì sẽ không triển khai một interface nào (các class tiện ích không có dependency, không nhận dependency làm tham số của hàm, không chứa nghiệp vụ hay use case, là các đoạn code không đổi). Cho nên trong nhiều trường hợp, việc tao interface không thực sự liên quan đến tính thiết kế, mà cho phép mã nguồn nhận những lợi ích khác như testing, hoặc sẵn sàng cho những thay đổi mà chúng ta vốn không biết trước. Việc có một interface đứng sau vẫn đảm bảo việc sửa đổi, mở rộng an toàn và dễ dàng hơn. Nhưng việc luôn tạo interface cũng có thể khiến mã nguồn trông bị rác khi các interface này vô nghĩa, không được sử dụng ở đâu (dù cả trong unit testing).
DI không chỉ giúp đỡ lập trình viên trong trường hợp nêu trên về database.
Cho nên, có thể nói lập trình viên luôn được hưởng lợi từ DI.
Đoạn code trên không phải Factory Method mà là Abstract Factory. Mặc dù hai mẫu này tên khá giống nhau nhưng lại có vai trò không liên quan đến nhau.Abstract Factory che giấu đi sự phức tạp của việc khởi tạo instance cho một họ. Bên tiêu dùng không cần biết hoặc không thể biết cách tạo một instance chứa hành vi mà họ cần, nên họ dựa vào Abstract Factory yêu cầu nó trả về đối tượng theo context được truyền vào.Factory Method để lại việc khởi tạo đối tượng nhất định cho bên tiêu dùng. Bên tiêu dùng biết cách tạo đối tượng cần thiết theo ngữ cảnh của bên tiêu dùng (kể cả việc gọi lại một Abstract Factory!). Factory Method không nhất thiết trả về các anh em. Nó có thể trả về một instance của class với cấu hình khác nhau. VD:Abstract Factory là một mẫu có độ phổ biến cao, nên tất nhiên chúng ta sẽ gặp nhiều trong thực tế. Mặc dù Dependency Injection làm rất tốt trong việc quản lý đối tượng, và thậm chí còn khiến Singleton biến mất, nhưng Abstract Factory vẫn tồn tại vì nó giúp code trong sáng hơn (mặc dù Abstract Factory hiện tại cũng chỉ gọi lại Dependency Injection container).Factory Method thì không phổ biến đến thế nhưng cũng không khó gặp. Hãy chú ý đến hành vi của các hàm trong class vì nó sẽ không được đặt tên là FactoryMethod. Có thể có nhiều lần chúng ta khởi tạo một đối tượng tùy theo context, nhưng chưa chắc chúng ta nghĩ ra Factory Method để áp dụng. Có nghĩa bài toán ở đó nhưng được giải theo cách khác.Phần bình luận về Factory Method là sai. Đoạn code trên không phải Abstract Factory mà là một biến thể của Factory Method, hay gần nghĩa hơn, chỉ là Factory (trong khi Factory Method nhấn mạnh Method và cài đặt sử dụng một phương thức). Cả hai cách cài đặt dùng phương thức và class của Factory Method đều cung cấp một gói lợi ích theo ngữ cảnh, nhưng có sự khác nhau rõ rệt trong cách cài đặt: