_Worker_of_MUFC_
Member
Mở đầu
Lời đầu tiên mình xin được chia sẻ là mình không có ý định "dạy đời" bất kì ai cả. Tất cả những điều mình đưa ra đều mang tính cá nhân của mình. Mọi gạch đá đều ok hết.
Có 1 thread khá hay về Project/Code Structure (link), tuy có hơi lan man, nhưng mình thấy đây là một vấn đề hay rất đáng để học hỏi. Do vậy nên mình sẽ chia sẻ kinh nghiệm của bản thân về vấn đề này. (Ban đầu mình định post lên Medium kiếm ít tiền, nhưng nghĩ đi nghĩ lại, ném vào đây có nhiều cao nhân ẩn dật hơn).
Các ví dụ mình đưa ra chủ yếu là dựa trên các Java web framework (Spring, Struts,...), tuy nhiên các bạn có thể map sang các framework của các ngôn ngữ khác một cách tương đương được. Về mặt bản chất thì ý tưởng không thay đổi.
Now, let's go
Level 1: Mô hình truyền thống
Mô hình web này chia cấu trúc project/code ra làm 3 phần riêng biệt: Controller, Service, DAO.
Cấu trúc code có thể chia ra như sauL
[name.of.company].form (entity model for UI)
|_____function_A_Form
|_____function_B_Form
|_____...
[name.of.company].formValidator
|_____function_A_FormValidator
|_____function_B_FormValidator
|_____...
[name.of.company].controller
|_____function_A_Controller
|_____function_B_Controller
|_____...
[name.of.company].service
|_____function_A_Service (Là 1 interface)
|_____function_A_ServiceImpl (Là implement của function_A_Service)
|_____function_B_Service (Là 1 interface)
|_____function_B_ServiceImpl (Là implement của function_B_Service)
|_____...
[name.of.company].dao (persistence layer)
|_____function_A_DAO (Là 1 interface)
|_____function_A_DAOImpl (Là implement của function_A_DAO)
|_____function_B_DAO (Là 1 interface)
|_____function_B_DAOImpl (Là implement của function_B_DAO)
|_____...
[name.of.company].entity (entity model for persistence layer)
|_____table_X_Object
|_____table_Y_Object
|_____...
Đây là mô hình tương đối phổ biến. Chỉ cần google example của Spring MVC là sẽ thấy hầu hết các example đều làm theo mô hình này. Việc phân chia code theo cách này tương đối rõ ràng, cấu trúc code cũng hợp lý, dễ quản lý. Nhưng:
---Nhược điểm chí mạng của mô hình này chính là sự chồng chéo nhau giữa các functions. Về mặt ý tưởng thì chúng ta luôn muốn chia các functions một cách độc lập nhất có thể. Nhưng trên thực tế, với các business logic phức tạp luôn có sự overlap lẫn nhau giữa các function --> Điều này sẽ làm cho việc maintain khi project phình to cực kì painful.
Ngoài ra còn có một số nhược điểm khác mà mình sẽ nói ở phần sau.
Level 1+: Mô hình truyền thống cải tiến
Có một điểm mà mình thấy thực sự không cần thiết ở mô hình level 1 chính là sự xuất hiện của các Interfaces. Về mặt idea thì chắc chắn ai cũng hiểu mục đích của việc tạo ra Interfaces để làm gì. Nhưng trên thực tế, mình thấy 99% các trường hợp chẳng cần dùng tới bọn Interfaces này làm gì
--> Mô hình cải tiến sẽ loại bỏ các redundant code.
Level 2: Mô hình của mình
Mô hình này sẽ giải quyết vấn đề chồng chéo code ở mô hình level 1.
Trước giờ hầu hết các projects đều chia nhỏ dưới dạng functions/module, cái này là đúng nhưng chưa đủ. Idea của mình đơn giản chỉ là cải tiến hơn. Mình sẽ chia ra đơn vị nhỏ nhất là UI Screen. Do các Screen là độc lập với nhau nên mình sẽ đảm bảo được tính độc lập giữa các modules.
Thêm một phần nữa, trong mô hình truyền thống, project/code structure được chia theo chức năng --> Việc traverse giữa một đống packages như thế thực sự không vui một chút nào.
Và đây là project/code structure của một số dự án mình áp dụng
[name.of.company].screen.screenA
|_____screen_A_Form
|_____screen_A_FormValidator
|_____screen_A_Controller
|_____screen_A_Service
|_____screen_A_DAO
[name.of.company].screen.screenB
|_____screen_B_Form
|_____screen_B_FormValidator
|_____screen_B_Controller
|_____screen_B_Service
|_____screen_B_DAO
....
[name.of.company].entity (entity model for persistence layer)
|_____table_X_Object
|_____table_Y_Object
|_____...
Việc focus vào một chỗ nhỏ trong project sẽ tốt hơn rất nhiều việc phân chia code chỗ này một tí, chỗ kia một tí (Bác nào dùng vim/nvim chắc chắn sẽ đồng ý với mình về vụ này). Việc manage source control cũng dễ thở hơn nhiều. Các modules (ở đây là screens) chỉ có "dính" với nhau duy nhất ở phần Controller (thì cũng phải có cái gì liên kết chứ)
Nhưng cái gì cũng có mặt không tốt của nó. Mô hình của mình cũng vậy. Nhược điểm của mô hình này của mình chính là việc duplicate code diễn ra tương đối nhiều.
Ví dụ: cùng là 1 câu query SQL, nhưng có thể xuất hiện cả ở screen A lẫn screen B (Để đảm bảo tính độc lập giữa các screens với nhau, logic sẽ được tách riêng là điều đương nhiên)
Trên quan điểm của mình, việc duplicate code này không ảnh hưởng gì tới software quality cả. Và trên thực tế, mình phản đối việc re-use các code có liên quan tới business logic. Bản chất mỗi business đều độc lập với nhau, việc trùng nhau là chuyện bình thường. Khi tách riêng ra, việc maintain sẽ cực kì dễ thở vì không lo ảnh hưởng tới các module khác.
Level 2+: Mô hình cải tiến
Mô hình này được phát triển khi mình gặp một dự án quái thai. KH muốn chuyển đổi từ .NET MVC sang Java, cụ thể là Struts 2. Dự án đang đi được nửa đường, KH đổi ý, ko muốn sang Struts nữa mà sang Spring MVC. Việc chuyển đổi logic từ cũ -> mới -> mới hơn thực sự là đau đầu. Và ý tưởng về việc tập trung lại business logic tại một chỗ ra đời.
Ở mô hình level 2, các bạn có thể thấy tuy tương đối ok, nhưng business logic đang bị phân mảnh tại 2 chỗ: FormValidator và DAO.
FormValidator sẽ thực hiện các heavy validation (việc check từng field xem null/empty,... không tính tiền), nó cũng là một business logic tương đối quan trọng.
DAO: Nghe thì đơn giản chỉ là các câu query, nhưng mỗi cái đều có cái lý của nó. Câu chuyện về DAO thì bản thân mình trải nghiệm kha khá.
Thời kì đầu (trước những năm 2000): hầu hết các cty tập trung business logic ở các câu query (có câu query dài tới 4 trang A4 nếu in ra)--> Do giới hạn về design.
Thời kì sau (trước năm 2015): business logic được move dần sang phần code đễ dễ maintain hơn, tăng tính readablity --> Hầu như chúng ta sẽ gặp các trường hợp này. Phần PL chỉ còn nhiệm vụ CRUD bình thường, rất ít các query phức tạp.
Thời kì hiện đại: Việc xử lý data ở DB bao giờ cũng nhanh hơn so với xử lý trong code. Thế nên các app cần tính toán nhanh thì lại quay về thời kì đầu là chuyển dần các business logic vào trong DB nên các câu query sẽ dần dần phức tạp hơn tương đối nhiều.
Idea của mình cũng không phải mới, mọi người chắc cũng biết. Đó là mình move các phần logic FormValidator vào trong Service (cái này thì ez). Và cái quan trọng nhất là mình move toàn bộ các query từ DAO vào Service. Để làm việc này, mình đã phải tự phát triển một app gần tương tự như jOOQ (thậm chí là ngon hơn). Việc tập trung business logic code vào một chỗ như thế này có vài điểm lợi như sau:
Level 3: Simplicity is the ultimate sophistication (Đơn giản là đỉnh cao của sự tinh tế)
Trước kia web app thường là server rendering với jsp, thymeleaf,...Công việc làm web sẽ focus tương đối vào cách transit pages và các logic kèm theo. Nhưng từ khi bọn React, Angular,...nó thống trị làng web thì cơ chế phía back-end cũng phải thay đổi theo. Giờ bọn back-end gần như chỉ tương đương lũ repo, cung cấp các RESTful APIs cho lũ front-end làm việc. Nếu áp dụng mô hình cũ thì có rất nhiều cái để đánh cãi chửi nhau (thực tế luôn).
Mình đã cực kì đau đầu vì vụ này. Bàn với một anh Team Lead (một fan cuồng của functional programing) thì lão còn định đập cmn server Spring đi mà thay thế = NodeJS (KH nó lại chả tế sống cả team). Cuối cùng cả 2 cũng đưa ra một giải pháp khá hay. Đó là thay vì phải đau đầu phân chia theo screen thì ném tất cả vào một chỗ. Kết quả là: 1 Controller, 1 Service, 1 DAO.
Nhiều bạn đọc đến đây sẽ chửi tung lên ngay. Nhưng xin đọc hết đã nhé.
Cách phân chia này sẽ đảm bảo được tính độc lập trong code(moẹ, có mỗi một thì có conflict với ai đâu mà chả độc lập), lại khắc phục được vụ duplicate code như trong mô hình level 2.
Nhưng cách làm này nếu áp dụng source control theo cách truyền thống thì bọn TL với PM như tôi chết cmn luôn. Suy nghĩ chán chê cuối cùng tự thấy mình ngu. Tại sao phải bổ dọc (tức là mỗi dev sẽ làm từ A-Z cho 1 function/module từ UI, Controller, Service, DAO) khi mà cách chia của mình đã làm theo từng lớp? Thế là tôi phân chia như sau:
Kết quả là dự án chạy vèo vèo, mình còn giữ được thành tích là 1 năm liền deliver bug free product cho KH. Như vậy nó cũng tương đối có hiệu quả đấy chứ.
Còn về project/code structure bên front-end thế nào thì còn nhiều cái khá phức tạp, bản thân mình cũng ko có quá nhiều exp về cái này nên ko dám chém mạnh. Nhưng bạn Lý Thành Nhân bên Tiki cũng có 2 bài tương đối hay để cho mọi người tham khảo:
https://nextlint.com/@lythanhnhan27...-ung-dung-react-phan-1-eHHLqvVXxUzFy48phJMspf
https://nextlint.com/@lythanhnhan27...n-hao-cho-react-phan-2-C7k235L64BWzKNQ9Rzt2PU
Nào, giờ có gạch đá gì thì mình xin chịu tất nhé. Lưu ý: gạch đá thì phải có lý luận và dẫn chứng cụ thể nhé. Thanks for reading
Lời đầu tiên mình xin được chia sẻ là mình không có ý định "dạy đời" bất kì ai cả. Tất cả những điều mình đưa ra đều mang tính cá nhân của mình. Mọi gạch đá đều ok hết.
Có 1 thread khá hay về Project/Code Structure (link), tuy có hơi lan man, nhưng mình thấy đây là một vấn đề hay rất đáng để học hỏi. Do vậy nên mình sẽ chia sẻ kinh nghiệm của bản thân về vấn đề này. (Ban đầu mình định post lên Medium kiếm ít tiền, nhưng nghĩ đi nghĩ lại, ném vào đây có nhiều cao nhân ẩn dật hơn).
Các ví dụ mình đưa ra chủ yếu là dựa trên các Java web framework (Spring, Struts,...), tuy nhiên các bạn có thể map sang các framework của các ngôn ngữ khác một cách tương đương được. Về mặt bản chất thì ý tưởng không thay đổi.
Now, let's go
Level 1: Mô hình truyền thống
Mô hình web này chia cấu trúc project/code ra làm 3 phần riêng biệt: Controller, Service, DAO.
Cấu trúc code có thể chia ra như sauL
[name.of.company].form (entity model for UI)
|_____function_A_Form
|_____function_B_Form
|_____...
[name.of.company].formValidator
|_____function_A_FormValidator
|_____function_B_FormValidator
|_____...
[name.of.company].controller
|_____function_A_Controller
|_____function_B_Controller
|_____...
[name.of.company].service
|_____function_A_Service (Là 1 interface)
|_____function_A_ServiceImpl (Là implement của function_A_Service)
|_____function_B_Service (Là 1 interface)
|_____function_B_ServiceImpl (Là implement của function_B_Service)
|_____...
[name.of.company].dao (persistence layer)
|_____function_A_DAO (Là 1 interface)
|_____function_A_DAOImpl (Là implement của function_A_DAO)
|_____function_B_DAO (Là 1 interface)
|_____function_B_DAOImpl (Là implement của function_B_DAO)
|_____...
[name.of.company].entity (entity model for persistence layer)
|_____table_X_Object
|_____table_Y_Object
|_____...
Đây là mô hình tương đối phổ biến. Chỉ cần google example của Spring MVC là sẽ thấy hầu hết các example đều làm theo mô hình này. Việc phân chia code theo cách này tương đối rõ ràng, cấu trúc code cũng hợp lý, dễ quản lý. Nhưng:
---Nhược điểm chí mạng của mô hình này chính là sự chồng chéo nhau giữa các functions. Về mặt ý tưởng thì chúng ta luôn muốn chia các functions một cách độc lập nhất có thể. Nhưng trên thực tế, với các business logic phức tạp luôn có sự overlap lẫn nhau giữa các function --> Điều này sẽ làm cho việc maintain khi project phình to cực kì painful.
Ngoài ra còn có một số nhược điểm khác mà mình sẽ nói ở phần sau.
Level 1+: Mô hình truyền thống cải tiến
Có một điểm mà mình thấy thực sự không cần thiết ở mô hình level 1 chính là sự xuất hiện của các Interfaces. Về mặt idea thì chắc chắn ai cũng hiểu mục đích của việc tạo ra Interfaces để làm gì. Nhưng trên thực tế, mình thấy 99% các trường hợp chẳng cần dùng tới bọn Interfaces này làm gì
--> Mô hình cải tiến sẽ loại bỏ các redundant code.
Level 2: Mô hình của mình
Mô hình này sẽ giải quyết vấn đề chồng chéo code ở mô hình level 1.
Trước giờ hầu hết các projects đều chia nhỏ dưới dạng functions/module, cái này là đúng nhưng chưa đủ. Idea của mình đơn giản chỉ là cải tiến hơn. Mình sẽ chia ra đơn vị nhỏ nhất là UI Screen. Do các Screen là độc lập với nhau nên mình sẽ đảm bảo được tính độc lập giữa các modules.
Thêm một phần nữa, trong mô hình truyền thống, project/code structure được chia theo chức năng --> Việc traverse giữa một đống packages như thế thực sự không vui một chút nào.
Và đây là project/code structure của một số dự án mình áp dụng
[name.of.company].screen.screenA
|_____screen_A_Form
|_____screen_A_FormValidator
|_____screen_A_Controller
|_____screen_A_Service
|_____screen_A_DAO
[name.of.company].screen.screenB
|_____screen_B_Form
|_____screen_B_FormValidator
|_____screen_B_Controller
|_____screen_B_Service
|_____screen_B_DAO
....
[name.of.company].entity (entity model for persistence layer)
|_____table_X_Object
|_____table_Y_Object
|_____...
Việc focus vào một chỗ nhỏ trong project sẽ tốt hơn rất nhiều việc phân chia code chỗ này một tí, chỗ kia một tí (Bác nào dùng vim/nvim chắc chắn sẽ đồng ý với mình về vụ này). Việc manage source control cũng dễ thở hơn nhiều. Các modules (ở đây là screens) chỉ có "dính" với nhau duy nhất ở phần Controller (thì cũng phải có cái gì liên kết chứ)
Nhưng cái gì cũng có mặt không tốt của nó. Mô hình của mình cũng vậy. Nhược điểm của mô hình này của mình chính là việc duplicate code diễn ra tương đối nhiều.
Ví dụ: cùng là 1 câu query SQL, nhưng có thể xuất hiện cả ở screen A lẫn screen B (Để đảm bảo tính độc lập giữa các screens với nhau, logic sẽ được tách riêng là điều đương nhiên)
Trên quan điểm của mình, việc duplicate code này không ảnh hưởng gì tới software quality cả. Và trên thực tế, mình phản đối việc re-use các code có liên quan tới business logic. Bản chất mỗi business đều độc lập với nhau, việc trùng nhau là chuyện bình thường. Khi tách riêng ra, việc maintain sẽ cực kì dễ thở vì không lo ảnh hưởng tới các module khác.
Level 2+: Mô hình cải tiến
Mô hình này được phát triển khi mình gặp một dự án quái thai. KH muốn chuyển đổi từ .NET MVC sang Java, cụ thể là Struts 2. Dự án đang đi được nửa đường, KH đổi ý, ko muốn sang Struts nữa mà sang Spring MVC. Việc chuyển đổi logic từ cũ -> mới -> mới hơn thực sự là đau đầu. Và ý tưởng về việc tập trung lại business logic tại một chỗ ra đời.
Ở mô hình level 2, các bạn có thể thấy tuy tương đối ok, nhưng business logic đang bị phân mảnh tại 2 chỗ: FormValidator và DAO.
FormValidator sẽ thực hiện các heavy validation (việc check từng field xem null/empty,... không tính tiền), nó cũng là một business logic tương đối quan trọng.
DAO: Nghe thì đơn giản chỉ là các câu query, nhưng mỗi cái đều có cái lý của nó. Câu chuyện về DAO thì bản thân mình trải nghiệm kha khá.
Thời kì đầu (trước những năm 2000): hầu hết các cty tập trung business logic ở các câu query (có câu query dài tới 4 trang A4 nếu in ra)--> Do giới hạn về design.
Thời kì sau (trước năm 2015): business logic được move dần sang phần code đễ dễ maintain hơn, tăng tính readablity --> Hầu như chúng ta sẽ gặp các trường hợp này. Phần PL chỉ còn nhiệm vụ CRUD bình thường, rất ít các query phức tạp.
Thời kì hiện đại: Việc xử lý data ở DB bao giờ cũng nhanh hơn so với xử lý trong code. Thế nên các app cần tính toán nhanh thì lại quay về thời kì đầu là chuyển dần các business logic vào trong DB nên các câu query sẽ dần dần phức tạp hơn tương đối nhiều.
Idea của mình cũng không phải mới, mọi người chắc cũng biết. Đó là mình move các phần logic FormValidator vào trong Service (cái này thì ez). Và cái quan trọng nhất là mình move toàn bộ các query từ DAO vào Service. Để làm việc này, mình đã phải tự phát triển một app gần tương tự như jOOQ (thậm chí là ngon hơn). Việc tập trung business logic code vào một chỗ như thế này có vài điểm lợi như sau:
- Loại bỏ được kha khá các entity/model không cần thiết kiểu POJO như: BO, DO, VO,....(mấy thằng này gần như same same nhau, nhưng vì yêu cầu mà đẻ ra lắm thứ)
- Tập trung business logic code lại một chỗ thì việc focus, debug dễ hơn nhiều.
- Bọn devs bớt được kha khá công việc Unit Test (thứ mình cho là bullsh!t và nonsense), UT cũng có ý nghĩa hơn.
- Gặp dev không quen thì sẽ bị choáng bởi cách phân chia không như truyền thống.
- Khi có bugs thì đúng là hơi mệt khi debug vì phải trace từ đầu tới cuối (cần dev vừa có exp, vừa phải hiểu business logic cái đang làm)
Level 3: Simplicity is the ultimate sophistication (Đơn giản là đỉnh cao của sự tinh tế)
Trước kia web app thường là server rendering với jsp, thymeleaf,...Công việc làm web sẽ focus tương đối vào cách transit pages và các logic kèm theo. Nhưng từ khi bọn React, Angular,...nó thống trị làng web thì cơ chế phía back-end cũng phải thay đổi theo. Giờ bọn back-end gần như chỉ tương đương lũ repo, cung cấp các RESTful APIs cho lũ front-end làm việc. Nếu áp dụng mô hình cũ thì có rất nhiều cái để đánh cãi chửi nhau (thực tế luôn).
Mình đã cực kì đau đầu vì vụ này. Bàn với một anh Team Lead (một fan cuồng của functional programing) thì lão còn định đập cmn server Spring đi mà thay thế = NodeJS (KH nó lại chả tế sống cả team). Cuối cùng cả 2 cũng đưa ra một giải pháp khá hay. Đó là thay vì phải đau đầu phân chia theo screen thì ném tất cả vào một chỗ. Kết quả là: 1 Controller, 1 Service, 1 DAO.
Nhiều bạn đọc đến đây sẽ chửi tung lên ngay. Nhưng xin đọc hết đã nhé.
Cách phân chia này sẽ đảm bảo được tính độc lập trong code(moẹ, có mỗi một thì có conflict với ai đâu mà chả độc lập), lại khắc phục được vụ duplicate code như trong mô hình level 2.
Nhưng cách làm này nếu áp dụng source control theo cách truyền thống thì bọn TL với PM như tôi chết cmn luôn. Suy nghĩ chán chê cuối cùng tự thấy mình ngu. Tại sao phải bổ dọc (tức là mỗi dev sẽ làm từ A-Z cho 1 function/module từ UI, Controller, Service, DAO) khi mà cách chia của mình đã làm theo từng lớp? Thế là tôi phân chia như sau:
- 1 team UI làm front-end, team này cử ra 1 thằng chỉ take care thằng Controller. Bọn UI cần cái gì thì báo với thằng làm Controller là xong. Thằng đấy cũng làm nhiệm vụ take care phần service trong UI (React/Angular) luôn.
- 1 team chỉ chuyên làm back-end Service, tập trung làm những thứ cần thiết và theo yêu cầu của bọn Controller mà thôi. Chỉ duy nhất Team Lead của team này được phép push code vào dev branch. Còn việc merge/resolve/...của chúng nó ntn thì mình ko care.
Kết quả là dự án chạy vèo vèo, mình còn giữ được thành tích là 1 năm liền deliver bug free product cho KH. Như vậy nó cũng tương đối có hiệu quả đấy chứ.
Còn về project/code structure bên front-end thế nào thì còn nhiều cái khá phức tạp, bản thân mình cũng ko có quá nhiều exp về cái này nên ko dám chém mạnh. Nhưng bạn Lý Thành Nhân bên Tiki cũng có 2 bài tương đối hay để cho mọi người tham khảo:
https://nextlint.com/@lythanhnhan27...-ung-dung-react-phan-1-eHHLqvVXxUzFy48phJMspf
https://nextlint.com/@lythanhnhan27...n-hao-cho-react-phan-2-C7k235L64BWzKNQ9Rzt2PU
Nào, giờ có gạch đá gì thì mình xin chịu tất nhé. Lưu ý: gạch đá thì phải có lý luận và dẫn chứng cụ thể nhé. Thanks for reading
.
, đơn giản vậy thôi