+1

Scaling Android Architecture #2: Android modularization from MVP to Enterprise

Mayfest2023

Tất cả chúng ta đều đã nghe ít nhất một lần về tầm quan trọng của việc mô đun hóa (modularization). Có rất nhiều lời chỉ trích xung quanh một mô-đun ứng dụng duy nhất (a single app module) và rất nhiều sự cường điệu xung quanh việc chia nó thành nhiều mô-đun.

Nhưng ở đây câu hỏi là. Tôi nên chia codebase của mình thành cấu trúc nhiều mô-đun như thế nào? Internet có rất nhiều bài viết và hướng dẫn khác nhau, cố gắng đề xuất một giải pháp lý tưởng duy nhất. Gần đây, ngay cả Google cũng tham gia cuộc thi và xuất bản hướng dẫn chính thức về mô đun hóa ứng dụng.

Cái bẫy của kỹ thuật thừa (Trap of overengineering)

Khi chúng ta thấy tất cả những hướng dẫn khác nhau này, chúng ta có thể rất bối rối vì những cách tiếp cận này có xu hướng rất khác nhau. Ở đây chúng ta thường rơi vào cái bẫy của kỹ thuật thừa. Chúng ta chọn giải pháp tiên tiến nhất vì chúng ta tin rằng giải pháp càng tiên tiến thì càng tốt.

Sau một thời gian dành cho việc triển khai giải pháp nâng cao này, chúng ta bắt đầu thấy các dấu hiệu đầu tiên của việc sử dụng quá kỹ thuật. Việc thêm các tính năng mới hoặc thay đổi các tính năng hiện có trở nên khó khăn. Đồng thời, không phải mọi dev trong team đều hiểu toàn bộ khái niệm mô-đun và không thể áp dụng nó đúng cách. Chúng ta thấy mình dành nhiều thời gian để duy trì cấu trúc mô-đun. Chúng ta bắt đầu tự hỏi bản thân - việc mô đun hóa có thực sự đáng để nỗ lực không?

Nâng cao không phải lúc nào cũng có thể mở rộng (Advanced is not always scalable)

Khi chúng ta thấy các triệu chứng như vậy thì chẩn đoán luôn giống nhau — phương pháp mô đun hóa mà chúng ta đã chọn quá phức tạp đối với dự án của chúng ta.

Kiến trúc của dự án phải phù hợp với nhu cầu cụ thể của nó. Nếu chúng ta có 3 developer xây dựng ứng dụng MVP thì không cần phải triển khai 20 mô-đun khác nhau.

Mặt khác, khi chúng ta có 20 developer và chúng ta xây dựng một full scale app thì chúng ta không thể chỉ dựa vào 3 mô-đun.

Vậy, khi nào kiến trúc là có thể mở rộng?

Ở đây tôi thường cung cấp một định nghĩa rất đơn giản:

Kiến trúc có thể mở rộng khi chúng ta có thể dễ dàng đưa ra các thay đổi đối với code

Thay đổi có nghĩa là thêm các tính năng mới, cập nhật các tính năng hiện có, giới thiệu tích hợp 3rd party, v.v. Nếu kiến trúc quá phức tạp, thì team không thể quản lý sự phức tạp đó và bất kỳ thay đổi nào cũng trở nên khó khăn. Và nếu kiến trúc quá đơn giản thì team không thể làm việc song song và dẫn đến nhiều conflict.

Bắt đầu đơn giản và sử dụng các packages 📁

Tất cả chúng ta đều nghĩ về các mô-đun nhưng đôi khi chúng ta quên mất các packages. Chúng đơn giản hơn nhiều để sử dụng so với các mô-đun đầy đủ. Chúng ta có thể dễ dàng thêm, xóa, đổi tên hoặc di chuyển code từ package này sang package khác.

Single module cùng với các packages là hoàn hảo cho một MVP

Tại thời điểm này, chúng ta vẫn chưa chắc ứng dụng của mình sẽ có những tính năng nào. Các requirements chưa hoàn thiện và chúng thường thay đổi trong lặng lẽ.

Ngoài ra team thường tương đối nhỏ. Chúng ta có thể xây dựng một MVP với một team gồm 2–3 mobile developer. Chúng ta không được assigned tới bất kỳ business context cụ thể nào và chúng ta hợp tác chặt chẽ để deliver phiên bản đầu tiên của sản phẩm.

Không cần thiết phải tạo nhiều hơn một app module trong giai đoạn này. Tuy nhiên, chúng ta nên giữ cho kiến trúc của mình có khả năng mở rộng và phân chia nó hợp lý bằng cách sử dụng các packages.

Chia app module thành các layers 💚

Điểm khởi đầu tốt nhất là tách các architectural layers chính bên trong app module. Đối với các ứng dụng CRUD đơn giản, chúng ta có thể có ba layers. Đối với những cái phức tạp hơn có liên quan đến business logic bổ sung, chúng ta thường tạo một thiết lập với một domain layer bổ sung. image.png

Chia từng layer thành các logical contexts nhỏ hơn 💚

Các Layers không chỉ là thùng cho các classes. Mỗi layer nên được chia thành các logical contexts nhỏ hơn. Bằng cách này, chúng ta có thể dễ dàng phân biệt các mảnh riêng lẻ của chúng.

  • UI Layer có thể được chia cho các screens.
  • Các Domain và Data layer có thể được chia theo các loại Entities.
  • Core layer có thể được chia theo 3rd party hoặc API system.

image.png

Làm theo đề xuất của Google nhưng sử dụng các packages 💚

Tại thời điểm này, bạn có thể phát hiện ra rằng phương pháp của chúng ta tuân theo các khuyến nghị chính thức của Google về mô đun hóa. Chúng ta phân biệt các layer chính và chia từng layer thành các contexts nhỏ hơn.

Lúc đầu, chúng ta làm điều đó với các packages, không phải với các mô-đun, chỉ để không làm phức tạp quá mức một kiến trúc. Khi chúng ta có sự phân tách tốt với các packages, chúng ta có thể dễ dàng mở rộng quy mô khi dự án và team của chúng ta phát triển.

https://developer.android.com/topic/modularization

Đừng chia các layers thành nhiều layers 🔴

Một sai lầm phổ biến là chia các layer thành nhiều layers hơn thay vì context hợp lý. Loại tách này không scale tốt.

image.png

Việc sửa đổi code sẽ dễ dàng hơn nhiều khi nó được grouped theo context hợp lý. Khi chúng ta cần thay đổi cách xử lý session, chúng ta cần có quyền truy cập vào Session model, SessionRepositorySessionApi. Chúng ta muốn có tất cả chúng ở gần nhau trong một package duy nhất.

Đồng thời, khi chúng ta làm việc với các thay đổi của Session, chúng ta không cần phải chạm vào Article model hoặc ArticlesRepostory nên chúng nên được tách riêng trong một package khác.

Không chia ứng dụng thành các screen-driven features 🔴

Lỗi thứ hai mà tôi thường thấy trong các dự án là chia nhỏ ứng dụng theo các features trong đó mỗi feature nằm trong phạm vi một screen. image.png Giả sử rằng mỗi màn hình trong ứng dụng là một tính năng độc lập sẽ sớm gây ra nhiều vấn đề hơn chúng ta nghĩ.

Khi mỗi màn hình được coi là một tính năng, chúng ta cố gắng tạo một domain và các data layers có cùng cách đặt tên. Do đó, việc triển domain và data của chúng ta được driven bởi UI trong khi chúng ta nên giữ cho UI bất khả tri.

Các screen-driven features mang lại sự tách biệt rất ảo tưởng. Khá hiếm khi có một Repository chỉ được sử dụng bởi một ViewModel duy nhất. Thông thường nó phải được truy cập bởi nhiều ViewModel. Để làm được điều đó, chúng ta cần truy cập data từ các features khác và toàn bộ sự tách biệt không còn nữa.

Scaling up with modules 📈

Good job 🎉 Chúng ta đã vượt qua phần khó khăn nhất. Chúng ta biết cách duy trì kiến trúc mô-đun tốt kể từ giai đoạn đầu của dự án. Tôi đã nói kiến trúc mô-đun, nhưng những mô-đun này ở đâu? Chúng ta chỉ có một cái duy nhất.

Ở đây có sức mạnh của scalability. Tất cả những điều Nên và Không nên mà chúng ta đã thảo luận lúc trước cho phép chúng ta dễ dàng mở rộng quy mô cấu trúc mô-đun theo nhu cầu của dự án. Và về cơ bản, chúng ta có 3 tùy chọn phù hợp với các tình huống khác nhau.

Small team with a growing app — Improved performance

Giả sử chúng ta đã hoàn thành giai đoạn MVP. Một kết quả là tích cực và khách hàng có nhiều ý tưởng và yêu cầu hơn để thực hiện. Phạm vi không lớn, thời hạn không chặt chẽ nên chúng ta quyết định giữ nguyên thành phần team hiện tại và không liên quan đến nhiều nhà phát triển hơn.

Tuy nhiên, chúng ta thấy rằng thời gian CI đã mất vài phút và chúng ta muốn giữ thời gian này ở mức nhỏ khi thêm nhiều screens, flows và functionalities hơn. Ở đây, chúng ta nên hướng tới việc chuyển đổi các packages của mình thành các mô-đun. Chúng ta giữ chính xác cấu trúc như trước đây, chỉ sử dụng các mô-đun thay cho các packages. image.png

Growing team with a growing app — Công việc song song được đơn giản hóa

MVP của chúng ta là một thành công lớn đến mức ứng dụng của chúng ta cần phải phát triển thật nhanh. Phạm vi sẽ được chia thành 4 team nhỏ hơn, mỗi nhóm có 3 dev. Chúng ta muốn mỗi team làm việc trên một phần khác nhau của scope.

Ở đây chúng ta cần một chiến lược mô đun hóa khác. Mục tiêu chính của chúng ta là giữ cho các team độc lập với nhau và cho phép họ có quyền sở hữu đối với các phần cụ thể của ứng dụng.

Đây là một nơi thích hợp cho việc mô đun hóa theo feature. Chúng ta cần ghi nhớ rằng feature không phải là screen. Feature thường đề cập đến một số business flow hoàn chỉnh hoặc một nhóm các flows liên quan.

  • Chúng ta chia scope của chúng ta thành nhiều features
  • Mỗi team có quyền sở hữu một hoặc nhiều features
  • Mỗi feature được chia thành các layers bằng cách sử dụng các packages, giống như chúng ta đã làm cho app module.
  • Mỗi layer của features được chia thành các context logic nhỏ hơn bằng cách sử dụng các packages, giống như chúng ta đã làm cho app module.

image.png

Đừng làm điều đó ngay lập tức. Scale nó từng bước

Chúng ta không cần phải thực hiện toàn bộ quá trình chuyển đổi ngay lập tức. Một chiến lược tốt là:

  1. Trước tiên, chỉ chuyển đổi một package core thành một mô-đun riêng biệt để chúng có thể được reused dễ dàng.
  2. Mọi functionality mới mà bạn thêm vào ứng dụng phải được tạo bằng mô-đun thay vì package.
  3. Khi bạn có một business request để làm điều gì đó với chức năng hiện có từ app module, bạn extract các packages liên quan sang một mô-đun riêng.

Cách tiếp cận này sẽ có chi phí tối thiểu được thêm vào các tasks mới và bạn sẽ tránh làm công ty sợ hãi với các tasks REFACTOR 😃 image.png

Kiến trúc cấp Enterprise 🏢 (Enterprise level architecture)

Các giải pháp trên là quá đủ cho hầu hết các dự án. Đi xa hơn có thể mang lại thêm sự phức tạp mà không có bất kỳ lợi ích đáng kể nào.

Nhưng có thể xảy ra trường hợp dự án của bạn đang hoạt động trên quy mô lớn. Mobile team không chỉ có 10 mà là 50 dev. Chúng không chỉ hoạt động song song trên các features khác nhau mà còn trên các phần khác nhau của cùng một feature.

Ứng dụng lớn đến mức một dev khó có thể hiểu được tất cả các chi tiết về kiến trúc của chúng ta và tất cả các phần của codebase. Chia sẻ kiến thức và tiêu chuẩn chung giữa hàng chục dev thường là một thách thức lớn.

Chia các layers riêng biệt bằng cách sử dụng các mô-đun

Chúng ta có thể thực thi thiết kế kiến trúc của mình bằng cách tách các layers trong mỗi feature. Chúng ta đã phân tách chúng bằng cách sử dụng các pakages nhưng việc phân tách theo pakages không quá nghiêm ngặt như phân tách theo mô-đun. Đồng thời, chúng ta giữ cho mỗi layer được chia thành các context nhỏ hơn bằng cách sử dụng các packages như chúng ta đã làm trong các phases trước. image.png

Tập hợp các features thành các groups hợp lý

Trong ứng dụng lớn, chúng ta có thể tính hàng chục features. Một số feature đề cập đến business context tương tự trong khi những feature khác đến từ một hành tinh hoàn toàn khác. Để cải thiện code organization và hiểu biết về dự án, chúng ta có thể nghĩ cách tập hợp các features thành một higher level groups. image.png

Summary

  1. Bắt đầu với cách tiếp cận mô-đun đơn giản nhất phù hợp với dự án của bạn và biến nó thành một cách có thể scalable.
  2. Hãy suy nghĩ xem bạn có thể giải quyết vấn đề của mình bằng các packages trước khi thêm các mô-đun khác hay không.
  3. Mở rộng quy mô kiến trúc của bạn khi team mở rộng hoặc thời gian build tăng lên đáng kể.
  4. Tránh refactoring toàn bộ ứng dụng cùng một lúc. Kiến trúc có thể được mở rộng từng bước khi có các yêu cầu mới.
  5. Khi bạn có nhiều dev và team hơn thì hãy phân chia scope, giới thiệu các feature mô-đun và chỉ định quyền sở hữu đối với chúng.
  6. Features không phải là screens. Features tốt chứa các business flows hoàn chỉnh.
  7. Giữ các feature độc lập. Các features lý tưởng không nên phụ thuộc vào các features khác. Trong thực tế, một feature đôi khi cần phụ thuộc vào một feature khác nhưng chúng ta nên giữ nó ở mức tối thiểu.
  8. Đừng đi sâu vào các enterprise solutions nếu dự án của bạn không hoạt động ở quy mô đó.
  9. Ở cấp độ enterprise, bạn có thể xem xét việc group các features của mình để hiểu rõ hơn và tách các layers bằng mô-đun để thực thi thiết kế kiến trúc.

Nguồn: https://medium.com/itnext/android-modularization-from-a-single-module-to-micro-services-6543531648a6


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí