Bài trước mình đã review và refactor một vài hàm trong bộ mã nguồn này. Bài này sẽ chỉ ra một số điểm về kiến trúc để chúng ta cải thiện.
Git: https://github.com/refacore/WebVella-ERP/tree/refactor/refactor-webapicontroller
Siêu controller
Nhìn vào WebApiController chúng ta sẽ hoa mắt một chút vì nó dài đến 4.5k dòng. Rất khó để có thể giải thích vì sao file này lại dài như thế. Có thể các tác giả nghĩ rằng gom hết các endpoint vào một file như thế này thì sẽ dễ quản lý (thêm mới, sửa đổi), nhưng thực chất nó gặp một số điểm bất lợi.
- Lifetime của controller là ScopedAsync, tức là được khởi tạo và giải phóng cùng với vòng đời của 1 request. Mỗi instance của controller cũng chỉ để gọi 1 action. Các dependency được inject hoặc khởi tạo trong constructor của controller vì thế sẽ thừa thãi nhiều nếu gom tất cả các action vào một controller như thế. Việc khởi tạo một dependency có thể rất tốn kém.
- Việc phát triển các chức năng mới có thể tiềm ẩn rủi ro. Khi các dependency mới được thêm vào, nếu dependency này bị lỗi sẽ khiến tất cả các chức năng chết cùng một lượt.
- Đọc code khó khăn. Một file dài thì việc đọc code tất nhiên khó khăn hơn, và các developer cũng thiếu tự tin hơn khi chỉnh sửa một file lớn.
Vậy một việc cần làm là tách các khối nghiệp vụ ra các controller khác nhau. Việc này chắc không thành vấn đề vì các action đã được nhóm lại theo chức năng.
Dependency Injection
Như chúng ta thấy thì constructor của controller có inject một số dependency, nhưng một số lại được khởi tạo trực tiếp. Có thể các dependency được inject vào là các dependency mới và các tác giả không muốn thay đổi các dòng code có trước (???).
Dependency Injection giúp chúng ta ẩn giấu đi cách thức chi tiết một instance được khởi tạo. Việc khởi tạo một dependency có thể rất phức tạp, ví dụ như A phụ thuộc vào B, B phụ thuộc vào C (chí ít với 3-tiers thì điều này là phổ biến). Vậy bất cứ thay đổi nào của B hoặc C đều khiến A bị lỗi. Sử dụng Dependency Injection giúp chúng ta loại bỏ các rắc rối ấy và chỉ quan tâm đến lợi ích mà dependency mang lại.
Để khắc phục điều này, chúng ta sẽ đăng kí các dependency của controller với DI container và inject nó trong constructor của controller là được.
Dependency Inversion
Các dependency được inject vào controller hiện tại đều là concrete class, các lớp thực thụ. Như vậy chương trình rất khó để:
- Mở rộng chức năng mà tối thiểu rủi ro. Việc sửa đổi trực tiếp các concreate class có thể ảnh hưởng tới rất nhiều nơi mà chúng ta không biết. Thừa kế các concreate class cũng không phải một việc được khuyến khích và cũng không thể thực hiện được nhiều lần (rắc rối về level of abstraction và overwrite - nạp chồng).
- Unit testing. Để viết unit test, các concreate class cần phải được cô lập. Việc phụ thuộc vào các concreate class khác không cho phép chúng ta cô lập một class và kết quả của unit test sẽ không được như ý muốn.
Để cải thiện, chúng ta sẽ tạo các interface cho các dependency và đăng ký với DI container.
Static methods
Các hàm static được sử dụng khá nhiều trong mã nguồn này. Hãy xem AuthService. Dường như OAuth được thêm vào sau này và người thêm đã quá lười để viết nó tử tế, thay vào đó tạo các hàm static. Mặc dù các hàm này có vẻ không sử dụng các dependency khác và chỉ là các hàm tính toán, nhưng việc gọi các hàm static này là việc tạo một phụ thuộc cứng và sẽ tạo các bất lợi như phần Dependency Inversion ở trên. Ngoài ra, nó còn dễ tạo ra loại tham chiếu chéo, nghĩa là A có tham chiếu đến B và B có tham chiếu đến A mà cả hai không biết.
Các hàm static chỉ nên là các hàm tính toán phụ trợ không chứa business logic, không gọi các dependency nào khác.
Để khắc phục, ta chuyển các hàm static này về các hàm thông thường.
Kết
Việc refactor đến cấu trúc cũng như kiến trúc mã có thể rất khó khăn. Các lớp liên kết cứng với nhau dẫn đến việc giải liên kết này liên đới nhiều thành phần trong mã nguồn. Sửa ở một class có thể kéo theo hiệu ứng dây chuyền đến một loạt class khác ở các layer khác nhau của mã nguồn. Nhưng việc này là việc phải làm bởi sự phức tạp khi thay đổi của nó cho tay thấy nó yếu kém thế nào. Khi chủ động cải tạo nó vẫn là tốt hơn so với việc đánh vật với nó do tính huống ép buộc trong tương lai.