Không có một chiến lược mô-đun hoá nào phù hợp với mọi dự án. Nhờ sự linh hoạt vốn có của Gradle nên bạn sẽ gặp rất ít hạn chế khi thực hiện việc tổ chức dự án. Trang này cung cấp thông tin tổng quan về một số quy tắc chung cũng như các mẫu phổ biến mà bạn có thể tận dụng khi phát triển ứng dụng Android đa mô-đun.
Nguyên tắc gắn kết chặt chẽ và ít ràng buộc
Một cách để mô tả đặc điểm cơ sở mã mô-đun là sử dụng các khái niệm ràng buộc và gắn kết. Khái niệm "ràng buộc" đo lường mức độ phụ thuộc lẫn nhau của các mô-đun. Trong ngữ cảnh này, khái niệm "gắn kết" đo lường sự liên kết về chức năng giữa các thành phần của một mô-đun. Nguyên tắc chung là chương trình của bạn nên có mức độ gắn kết cao và ràng buộc thấp:
- Ràng buộc thấp có nghĩa là mô-đun càng ít phụ thuộc vào các mô-đun khác càng tốt. Nhờ đó, những thay đổi thực hiện trên một mô-đun sẽ ít hoặc không ảnh hưởng đến các mô-đun khác. Mô-đun không nên có tri thức về hoạt động nội tại của các mô-đun khác.
- Độ gắn kết cao nghĩa là bộ mã trong mô-đun phải giống như một hệ thống. Các mô-đun này phải có trách nhiệm rõ ràng và luôn tuân thủ các giới hạn về nghiệp vụ nhất định của chúng. Hãy cùng xem xét một ứng dụng sách điện tử mẫu. Việc kết hợp các mã lập trình liên quan đến thanh toán và sách trong cùng một mô-đun có thể không phù hợp vì hai mã này là hai chức năng thuộc lĩnh vực khác nhau.
Các loại mô-đun
Tuỳ thuộc vào cấu trúc ứng dụng mà bạn có thể sắp xếp các mô-đun của mình. Dưới đây là một số loại mô-đun phổ biến mà bạn có thể ra mắt trong ứng dụng của mình khi thực hiện theo cấu trúc ứng dụng đề xuất của chúng tôi.
Mô-đun dữ liệu
Mô-đun dữ liệu thường chứa kho lưu trữ, nguồn dữ liệu và các lớp của mô hình. Một mô-đun dữ liệu có 3 trách nhiệm chính là:
- Đóng gói tất cả dữ liệu và logic hoạt động của một lĩnh vực nhất định: Mỗi mô-đun dữ liệu phải có trách nhiệm xử lý dữ liệu của một lĩnh vực nhất định. Mô-đun dữ liệu này có thể xử lý nhiều kiểu dữ liệu, miễn là các kiểu dữ liệu đó có liên quan với nhau.
- Hiển thị kho lưu trữ dưới dạng API bên ngoài: API công khai của mô-đun dữ liệu phải là kho lưu trữ vì API này chịu trách nhiệm hiển thị dữ liệu cho phần còn lại của ứng dụng.
- Ẩn tất cả chi tiết quy trình triển khai và nguồn dữ liệu từ bên ngoài:
Chỉ các kho lưu trữ ở cùng một mô-đun mới có thể truy cập vào nguồn dữ liệu.
Nguồn dữ liệu này vẫn bị ẩn đối với bên ngoài. Bạn có thể thực thi việc này bằng cách sử dụng từ khoá chế độ hiển thị
private
hoặcinternal
của Kotlin.
Mô-đun tính năng
Tính năng là một phần riêng biệt trong chức năng của ứng dụng, thường tương ứng với một màn hình hoặc một loạt màn hình có liên quan chặt chẽ, chẳng hạn như quy trình đăng ký hoặc thanh toán. Nếu ứng dụng của bạn có thanh điều hướng ở dưới cùng, thì có khả năng mỗi đích đến là một tính năng.
Các tính năng liên kết với màn hình hoặc đích đến trong ứng dụng của bạn. Do đó, có thể chúng sẽ có một giao diện người dùng liên kết và ViewModel
để xử lý logic và trạng thái. Một tính năng đơn lẻ không nhất thiết phải được giới hạn ở một thành phần hiển thị hoặc đích đến điều hướng. Mô-đun tính năng phụ thuộc vào các mô-đun dữ liệu.
Mô-đun ứng dụng
Mô-đun ứng dụng là điểm truy cập đến ứng dụng. Các mô-đun này phụ thuộc vào các mô-đun tính năng và thường cung cấp tính năng điều hướng gốc. Một mô-đun ứng dụng đơn lẻ có thể được biên dịch thành một số tệp nhị phân nhờ các biến thể bản dựng.
Nếu ứng dụng của bạn nhắm đến nhiều loại thiết bị (chẳng hạn như ô tô, thiết bị đeo thông minh hay TV), hãy xác định một mô-đun ứng dụng cho mỗi loại thiết bị. Điều này giúp tách biệt các phần phụ thuộc dành riêng cho từng nền tảng.
Các mô-đun phổ biến
Các mô-đun phổ biến, còn gọi là mô-đun cốt lõi, chứa mã mà các mô-đun khác thường sử dụng. Các mô-đun này giúp giảm tình trạng thừa mã và không đại diện cho lớp cụ thể nào trong cấu trúc của một ứng dụng. Sau đây là ví dụ về các mô-đun phổ biến:
- Mô-đun giao diện người dùng: Nếu sử dụng các thành phần tuỳ chỉnh trên giao diện người dùng hoặc áp dụng các chi tiết xây dựng thương hiệu trong ứng dụng, bạn nên cân nhắc việc đóng gói bộ sưu tập tiện ích thành một mô-đun để tất cả tính năng có thể sử dụng lại. Điều này có thể giúp giao diện người dùng trở nên nhất quán trên các tính năng khác nhau. Ví dụ: nếu chủ đề của ứng dụng được tập trung vào một nơi, bạn có thể tránh việc dành hàng tấn thời gian để tái cấu trúc khi đổi mới thương hiệu.
- Mô-đun phân tích số liệu: Việc theo dõi thường được quyết định bởi các yêu cầu kinh doanh mà không cần quan tâm nhiều đến cấu trúc phần mềm. Trình theo dõi phân tích số liệu thường được sử dụng trong nhiều thành phần không liên quan. Nếu gặp phải trường hợp này, bạn nên có mô-đun phân tích số liệu chuyên dụng.
- Mô-đun mạng: Khi có nhiều mô-đun yêu cầu kết nối mạng, bạn có thể cân nhắc việc có một mô-đun chuyên cung cấp ứng dụng khách http. Việc này đặc biệt hữu ích khi ứng dụng khách yêu cầu cấu hình tuỳ chỉnh.
- Mô-đun tiện ích: Tiện ích, hay còn gọi là trình trợ giúp, thường là những đoạn mã nhỏ được sử dụng lại trên ứng dụng. Ví dụ về các tiện ích: trình trợ giúp kiểm thử, hàm định dạng đơn vị tiền tệ, trình xác thực email hoặc toán tử tuỳ chỉnh.
Mô-đun kiểm thử
Mô-đun kiểm thử là các mô-đun Android chỉ dùng cho mục đích kiểm thử. Các mô-đun này chứa mã kiểm thử, tài nguyên kiểm thử và các phần phụ thuộc kiểm thử chỉ cần thiết để chạy hoạt động kiểm thử chứ không cần thiết trong thời gian chạy của ứng dụng. Các mô-đun kiểm thử được tạo để tách mã dành riêng cho kiểm thử khỏi ứng dụng chính, giúp quản lý và duy trì mã mô-đun dễ dàng hơn.
Các trường hợp sử dụng mô-đun kiểm thử
Các ví dụ sau đây cho thấy những trường hợp triển khai mô-đun kiểm thử có thể đặc biệt có lợi:
Mã kiểm thử dùng chung: Nếu có nhiều mô-đun trong dự án và một số mã kiểm thử có thể áp dụng cho nhiều mô-đun, thì bạn có thể tạo một mô-đun kiểm thử để chia sẻ mã đó. Điều này có thể giúp giảm tình trạng trùng lặp và giúp duy trì mã kiểm thử của bạn dễ dàng hơn. Mã kiểm thử dùng chung có thể bao gồm các lớp hoặc hàm tiện ích, chẳng hạn như câu nhận định hoặc trình so khớp tuỳ chỉnh, cũng như dữ liệu kiểm thử (ví dụ: phản hồi JSON được mô phỏng).
Cấu hình bản dựng sạch hơn: Các mô-đun kiểm thử cho phép bạn có cấu hình bản dựng sạch hơn, vì chúng có thể có tệp
build.gradle
riêng. Bạn không phải làm lộn xộn tệpbuild.gradle
của mô-đun ứng dụng với các cấu hình chỉ liên quan đến hoạt động kiểm thử.Kiểm thử tích hợp: Bạn có thể sử dụng mô-đun kiểm thử để lưu trữ các chương trình kiểm thử tích hợp dùng để kiểm thử hoạt động tương tác giữa nhiều phần của ứng dụng, bao gồm giao diện người dùng, logic kinh doanh, yêu cầu mạng và các truy vấn cơ sở dữ liệu.
Ứng dụng có quy mô lớn: Các mô-đun kiểm thử đặc biệt hữu ích cho các ứng dụng có quy mô lớn với cơ sở mã phức tạp và nhiều mô-đun. Trong những trường hợp như vậy, các mô-đun kiểm thử có thể giúp cải thiện khả năng duy trì và sắp xếp mã.
Giao tiếp giữa mô-đun với mô-đun
Các mô-đun hiếm khi tồn tại một cách hoàn toàn tách biệt và thường dựa vào, cũng như giao tiếp với các mô-đun khác. Việc giữ mức độ ràng buộc thấp là rất quan trọng, ngay cả khi các mô-đun hoạt động cùng nhau và trao đổi thông tin thường xuyên. Đôi khi, bạn không nên cho hai mô-đun giao tiếp trực tiếp với nhau, như trong trường hợp ràng buộc về mặt cấu trúc. Thậm chí cũng có khả năng không thực hiện được việc này, chẳng hạn như với các phần phụ thuộc tuần hoàn.
Để khắc phục vấn đề này, bạn có thể có một mô-đun thứ ba để dàn xếp giữa hai mô-đun khác. Mô-đun dàn xếp có thể theo dõi thông báo từ cả hai mô-đun và chuyển tiếp các thông báo này khi cần. Trong ứng dụng mẫu của chúng ta, màn hình thanh toán cần biết cuốn sách nào cần mua mặc dù sự kiện bắt nguồn từ một màn hình khác, thuộc về một tính năng khác. Trong trường hợp này, mô-đun dàn xếp là mô-đun sở hữu biểu đồ điều hướng (thường là mô-đun ứng dụng). Trong ví dụ này, chúng ta sử dụng tính năng điều hướng để truyền dữ liệu từ tính năng trang chủ đến tính năng thanh toán bằng thành phần Điều hướng.
navController.navigate("checkout/$bookId")
Đích đến thanh toán sẽ nhận được mã nhận dạng của sách để làm đối số, cụ thể là để tìm nạp thông tin về sách. Bạn có thể sử dụng Ô điều khiển trạng thái đã lưu để truy xuất các đối số điều hướng bên trong ViewModel
của tính năng đích.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
Bạn không nên truyền các đối tượng dưới dạng đối số điều hướng. Thay vào đó, hãy sử dụng mã nhận dạng đơn giản mà các tính năng có thể sử dụng để truy cập và tải tài nguyên mong muốn từ lớp dữ liệu. Bằng cách này, bạn sẽ duy trì được ràng buộc thấp và không vi phạm nguyên tắc nguồn đáng tin cậy duy nhất.
Trong ví dụ bên dưới, cả hai mô-đun tính năng đều phụ thuộc vào cùng một mô-đun dữ liệu. Điều này giúp bạn giảm thiểu lượng dữ liệu mà mô-đun dàn xếp cần chuyển tiếp và duy trì ràng buộc giữa các mô-đun ở mức thấp. Thay vì truyền đối tượng, các mô-đun phải trao đổi mã nhận dạng gốc và tải tài nguyên từ một mô-đun dữ liệu dùng chung.
Đảo ngược phần phụ thuộc
Đảo ngược phần phụ thuộc là khi bạn sắp xếp mã sao cho mô-đun trừu tượng tách biệt với mô-đun triển khai cụ thể.
- Mô-đun trừu tượng: Một hợp đồng xác định cách các thành phần hoặc mô-đun trong ứng dụng tương tác với nhau. Các mô-đun trừu tượng xác định API của hệ thống, đồng thời chứa các giao diện và mô hình.
- Mô-đun triển khai cụ thể: Các mô-đun phụ thuộc vào mô-đun trừu tượng và triển khai hành vi của mô-đun trừu tượng.
Các mô-đun dựa vào hành vi được xác định trong mô-đun trừu tượng chỉ nên phụ thuộc vào mô-đun trừu tượng, thay vì các mô-đun triển khai cụ thể.
Ví dụ
Hãy tưởng tượng một mô-đun tính năng cần có một cơ sở dữ liệu để hoạt động. Mô-đun tính năng không liên quan đến cách triển khai cơ sở dữ liệu, mà cần có một cơ sở dữ liệu Room cục bộ hoặc bản sao Firestore từ xa. Mô-đun này chỉ cần lưu trữ và đọc dữ liệu ứng dụng.
Để đạt được điều này, mô-đun tính năng phụ thuộc vào mô-đun trừu tượng thay vì một phương thức triển khai cụ thể cho cơ sở dữ liệu. Mô-đun trừu tượng này xác định API Cơ sở dữ liệu của ứng dụng. Nói cách khác, mô-đun này đặt ra các quy tắc về cách tương tác với cơ sở dữ liệu. Nhờ đó, mô-đun tính năng có thể dùng bất kỳ cơ sở dữ liệu nào mà không cần biết thông tin triển khai cơ bản của cơ sở dữ liệu đó.
Mô-đun triển khai cụ thể cung cấp phương thức triển khai thực tế của các API được xác định trong mô-đun trừu tượng. Để làm được điều đó, mô-đun triển khai cũng phụ thuộc vào mô-đun trừu tượng.
Chèn phần phụ thuộc
Hiện tại, bạn có thể thắc mắc về cách kết nối mô-đun tính năng với mô-đun triển khai. Câu trả lời là Chèn phần phụ thuộc. Mô-đun tính năng không trực tiếp tạo thực thể cơ sở dữ liệu bắt buộc. Thay vào đó, mô-đun này chỉ định những phần phụ thuộc cần thiết. Sau đó, các phần phụ thuộc này được cung cấp bên ngoài, thường là trong mô-đun ứng dụng.
releaseImplementation(project(":database:impl:firestore"))
debugImplementation(project(":database:impl:room"))
androidTestImplementation(project(":database:impl:mock"))
Lợi ích
Sau đây là lợi ích của việc phân tách giữa API và mô-đun triển khai các API này:
- Khả năng hoán đổi cho nhau: Với sự phân tách rõ ràng giữa API và các mô-đun triển khai, bạn có thể tiến hành nhiều lượt triển khai cho cùng một API và chuyển đổi giữa các mô-đun đó mà không cần thay đổi mã sử dụng API. Điều này có thể đặc biệt có lợi trong các trường hợp mà bạn muốn cung cấp nhiều khả năng hoặc hành vi trong nhiều ngữ cảnh. Ví dụ: triển khai mô phỏng để kiểm thử so với triển khai trong thực tế để sản xuất.
- Phân tách: Sự phân tách có nghĩa là các mô-đun sử dụng đối tượng trừu tượng không phụ thuộc vào bất kỳ công nghệ cụ thể nào. Nếu sau này bạn chọn thay đổi cơ sở dữ liệu của mình từ Room thành Firestore thì sẽ dễ dàng hơn vì các thay đổi sẽ chỉ xảy ra trong mô-đun cụ thể thực hiện lệnh (mô-đun triển khai) và sẽ không ảnh hưởng đến các mô-đun khác dùng API của cơ sở dữ liệu.
- Khả năng kiểm thử: Việc phân tách các API khỏi mô-đun triển khai có thể hỗ trợ đáng kể cho việc kiểm thử. Bạn có thể viết các trường hợp kiểm thử theo hợp đồng API. Bạn cũng có thể sử dụng nhiều mô-đun triển khai để kiểm thử nhiều tình huống và trường hợp phức tạp, bao gồm cả mô-đun triển khai mô phỏng.
- Cải thiện hiệu suất của bản dựng: Khi bạn phân tách API và mô-đun triển khai thành nhiều mô-đun, các thay đổi trong mô-đun triển khai không buộc hệ thống xây dựng phải biên dịch lại các mô-đun phụ thuộc vào mô-đun API. Điều này giúp rút ngắn thời gian xây dựng và tăng năng suất, đặc biệt là trong các dự án lớn có thời gian xây dựng đáng kể.
Thời điểm phân tách
Bạn nên phân tách API khỏi các mô-đun triển khai trong các trường hợp sau đây:
- Khả năng đa dạng: Nếu bạn có thể triển khai các phần trong hệ thống theo nhiều cách, thì với một API rõ ràng, bạn sẽ có thể áp dụng các cách triển khai khác nhau. Ví dụ: bạn có thể dùng một hệ thống hiển thị sử dụng OpenGL hoặc Vulkan, hoặc một hệ thống thanh toán hoạt động với Play hoặc API thanh toán nội bộ.
- Nhiều ứng dụng: Nếu đang phát triển nhiều ứng dụng có chung khả năng dành cho nhiều nền tảng, bạn có thể xác định các API phổ biến và phát triển các mô-đun triển khai cụ thể cho từng nền tảng.
- Nhóm độc lập: Sự phân tách này cho phép nhiều nhà phát triển hoặc nhóm làm việc trên nhiều phần của cơ sở mã cùng một lúc. Nhà phát triển nên tập trung tìm hiểu các hợp đồng API và sử dụng hợp đồng đó đúng cách. Họ không cần lo lắng về chi tiết triển khai của các mô-đun khác.
- Cơ sở mã lớn: Khi cơ sở mã lớn hoặc phức tạp, việc phân tách API khỏi mô-đun triển khai sẽ giúp dễ quản lý mã hơn. Điều này giúp bạn có thể chia nhỏ cơ sở mã thành các đơn vị chi tiết, dễ hiểu và dễ bảo trì hơn.
Cách thức triển khai?
Để triển khai tính năng đảo ngược phần phụ thuộc, hãy làm theo các bước sau đây:
- Tạo mô-đun trừu tượng: Mô-đun này nên chứa các API (giao diện và mô hình) xác định hành vi của tính năng.
- Tạo mô-đun triển khai: Mô-đun triển khai phải dựa vào mô-đun API và triển khai hành vi của mô-đun trừu tượng.
- Tạo mô-đun cấp cao phụ thuộc vào mô-đun trừu tượng: Thay vì trực tiếp phụ thuộc vào mô-đun triển khai cụ thể, hãy tạo mô-đun phụ thuộc vào mô-đun trừu tượng. Mô-đun cấp cao không cần biết chi tiết triển khai mà chỉ cần hợp đồng (API).
- Cung cấp mô-đun triển khai: Cuối cùng, bạn cần cung cấp mô-đun triển khai thực tế cho các phần phụ thuộc. Việc triển khai cụ thể phụ thuộc vào cách thiết lập dự án, nhưng mô-đun ứng dụng thường là nơi thích hợp để làm việc này. Để cung cấp mô-đun triển khai, hãy chỉ định mô-đun này là phần phụ thuộc cho biến thể bản dựng đã chọn hoặc nhóm tài nguyên kiểm thử.
Các phương pháp chung hay nhất
Như đã đề cập từ đầu, có nhiều cách phù hợp để phát triển một ứng dụng nhiều mô-đun. Tương tự với việc có đa dạng cấu trúc phần mềm, có nhiều cách để mô-đun hoá ứng dụng. Tuy nhiên, các đề xuất chung sau đây có thể giúp mã của bạn dễ đọc, dễ bảo trì và dễ kiểm thử hơn.
Duy trì tính nhất quán của cấu hình
Mỗi mô-đun đều có mức hao tổn cấu hình riêng. Nếu số lượng mô-đun của bạn đạt đến một ngưỡng nhất định, thì việc quản lý tính nhất quán của cấu hình sẽ trở nên rất khó khăn. Chẳng hạn, điều quan trọng là các mô-đun phải sử dụng cùng một phiên bản của các phần phụ thuộc. Nếu bạn chỉ cần cập nhật một phiên bản phần phụ thuộc nhưng lại phải cập nhật số lượng lớn mô-đun thì đó không chỉ là sự hao công tốn sức mà còn là kẽ hở có thể phát sinh ra lỗi. Để giải quyết vấn đề này, bạn có thể sử dụng một trong các công cụ của Gradle để tập trung vào cấu hình của mình:
- Danh mục phiên bản là một danh sách kiểu phần phụ thuộc do Gradle tạo ra trong quá trình đồng bộ hoá. Đây là tâm điểm để khai báo tất cả phần phụ thuộc của bạn và có sẵn cho tất cả các mô-đun trong một dự án.
- Hãy sử dụng trình bổ trợ quy ước để chia sẻ logic bản dựng giữa các mô-đun.
Hiển thị càng ít càng tốt
Giao diện công khai của một mô-đun phải ở mức tối thiểu và chỉ hiển thị các mục thiết yếu. Điều này sẽ không làm rò rỉ chi tiết triển khai nào ra bên ngoài. Giới hạn mọi thứ trong phạm vi nhỏ nhất có thể. Dùng phạm vi hiển thị private
hoặc internal
của Kotlin để đặt các phần khai báo ở chế độ riêng tư đối với mô-đun. Khi khai báo phần phụ thuộc trong mô-đun, hãy ưu tiên implementation
hơn api
. Phần sau hiển thị các phần phụ thuộc bắc cầu cho người dùng mô-đun của bạn. Việc sử dụng phương thức triển khai có thể cải thiện thời gian xây dựng vì sẽ làm giảm số lượng mô-đun cần xây dựng lại.
Ưu tiên các mô-đun Kotlin và Java
Có 3 kiểu mô-đun thiết yếu mà Android Studio hỗ trợ:
- Mô-đun ứng dụng là điểm truy cập đến ứng dụng. Các mô-đun này có thể chứa mã nguồn, tài nguyên, tài sản và
AndroidManifest.xml
. Kết quả đầu ra của một mô-đun ứng dụng là một Android App Bundle (AAB) hoặc một Gói ứng dụng Android (APK). - Mô-đun thư viện có cùng nội dung với các mô-đun ứng dụng. Các mô-đun Android khác sử dụng các mô-đun thư viện làm phần phụ thuộc. Kết quả đầu ra của một mô-đun thư viện là một Android ARchive (AAR) có cấu trúc giống hệt với các mô-đun ứng dụng nhưng được biên dịch thành một tệp Android Archive (AAR) sau đó có thể được các mô-đun khác sử dụng làm phần phụ thuộc. Mô-đun thư viện cho phép bạn đóng gói và sử dụng lại cùng một logic và tài nguyên trên nhiều mô-đun ứng dụng.
- Thư viện Kotlin và Java không chứa tài nguyên Android, tài sản hoặc tệp kê khai nào.
Vì các mô-đun Android đi kèm với mức hao tổn, tốt nhất bạn nên sử dụng loại Kotlin hoặc Java nhiều nhất có thể.