Mô-đun hóa là nguyên tắc mà ứng dụng được lắp ráp từ các mô-đun độc lập, mỗi mô-đun chịu trách nhiệm cho một chức năng. Theo Android Developers, việc chia thành các mô-đun tăng tốc độ xây dựng nhờ biên dịch song song và cho phép các nhóm làm việc độc lập trên các phần khác nhau của ứng dụng. Kiến trúc mô-đun đã trở thành tiêu chuẩn cho các dự án di động lớn với hàng chục nhà phát triển.
Những điểm chính
Mô-đun hóa là cách tổ chức mã trong đó một ứng dụng bao gồm các mô-đun kết nối lỏng lẻo, mỗi mô-đun cung cấp một chức năng được xác định chặt chẽ thông qua giao diện công khai. Không giống như kiến trúc nguyên khối nơi tất cả các lớp nằm trong một dự án duy nhất, cách tiếp cận mô-đun chia mã thành các đơn vị xây dựng độc lập về mặt vật lý.
Mục tiêu chính của mô-đun hóa là quản lý độ phức tạp. Nhà phát triển có thể tập trung vào một mô-đun mà không cần ghi nhớ toàn bộ cơ sở mã. Mỗi mô-đun có lĩnh vực trách nhiệm riêng và có thể được phát triển, kiểm thử và triển khai độc lập với các mô-đun khác. Điều này đặc biệt có giá trị trong các dự án có 10+ nhà phát triển, nơi làm việc song song trên một khối nguyên khối dẫn đến xung đột hợp nhất thường xuyên.
Điều quan trọng là phân biệt mô-đun hóa với kiến trúc phân lớp. Các lớp (Presentation, Domain, Data) chia mã theo tiêu chí kỹ thuật, trong khi mô-đun chia theo tiêu chí chức năng. Một mô-đun “Hồ sơ người dùng” có thể chứa các lớp riêng bên trong. Trong thực tế, cách tiếp cận mô-đun và kiến trúc phân lớp được kết hợp: mỗi mô-đun có cấu trúc ba lớp riêng.
Mô-đun tính năng là loại mô-đun phổ biến nhất. Mỗi màn hình hoặc nhóm màn hình liên quan được tách thành mô-đun riêng: Onboarding, Profile, Settings, Feed. Một mô-đun tính năng chứa mọi thứ cần thiết để tính năng hoạt động: UI, logic nghiệp vụ, lớp dữ liệu. Ranh giới mô-đun được bảo vệ — các tính năng khác không thể truy cập các lớp nội bộ của nó.
Mô-đun cốt lõi chứa cơ sở hạ tầng chung: mạng, cơ sở dữ liệu, phân tích, hệ thống thiết kế. Chúng không phụ thuộc vào mô-đun tính năng, nhưng mô-đun tính năng phụ thuộc vào chúng. Sự tách biệt này đảm bảo rằng việc thay đổi SDK phân tích sẽ không ảnh hưởng đến lớp mạng và ngược lại. Các mô-đun cốt lõi được tái sử dụng giữa các tính năng mà không trùng lặp mã.
Mô-đun dùng chung chứa mã được sử dụng bởi nhiều tính năng: mô hình dữ liệu, tiện ích, hằng số, Chế độ xem tùy chỉnh. Vấn đề chính của mô-đun dùng chung là nguy cơ biến thành bãi chứa (“mô-đun linh tinh”) nơi mã không đồng nhất tích tụ theo thời gian. Nguyên tắc: một mô-đun dùng chung phải có chủ đề rõ ràng, ví dụ “shared-ui” hoặc “shared-models”.
Trên Android, các mô-đun dùng chung thường được tách thành thư viện với tiền tố lib: lib-network, lib-database, lib-ui-components. Trên iOS, chức năng tương tự được thực hiện bởi các Swift Packages nội bộ trong Workspace. Trong thực tế, các nhóm giới hạn số lượng mô-đun dùng chung ở mức 3–5 để tránh tạo ra mạng lưới phụ thuộc quá mức làm phức tạp việc xây dựng.
Các mô-đun kiểm thử riêng biệt cho phép chạy kiểm thử chỉ cho mô-đun đã thay đổi mà không chạy toàn bộ bộ kiểm thử. Điều này giảm thời gian đường ống CI/CD từ hàng giờ xuống còn vài phút. Sự cô lập cấp mô-đun đảm bảo SoC ở cấp độ xây dựng: một mô-đun lớp mạng không thể vô tình nhập thư viện UI trong kiểm thử của nó.
Mỗi mô-đun phải có API công khai được xác định rõ ràng. Trên Android, điều này đạt được thông qua bổ từ truy cập và api so với implementation trong Gradle. Trên iOS, thông qua bổ từ truy cập public/internal và các phụ thuộc được quản lý qua Package.swift. Giảm khả năng hiển thị xuống mức tối thiểu cần thiết là một thực hành chính của thiết kế mô-đun.
Gradle hỗ trợ kiến trúc mô-đun một cách tự nhiên: mỗi mô-đun là một đơn vị xây dựng riêng biệt với tệp build.gradle riêng. Các dự án Android sử dụng kết hợp một mô-đun ứng dụng (app) và một số mô-đun thư viện. Các mô-đun thư viện không thể chạy như một ứng dụng nhưng có thể được xuất bản dưới dạng AAR vào kho lưu trữ.
Một tính năng chính của Gradle là xây dựng song song các mô-đun độc lập. Nếu các mô-đun A, B và C không phụ thuộc lẫn nhau, Gradle biên dịch chúng đồng thời bằng tất cả các lõi CPU. Trong các dự án có 20+ mô-đun, điều này giảm thời gian xây dựng đầy đủ từ 15 xuống 3–5 phút. Xây dựng gia tăng của một mô-đun đã thay đổi mất vài giây.
Gradle cung cấp hai loại phụ thuộc giữa các mô-đun: api (truyền) và implementation (không truyền). Sự khác biệt rất quan trọng đối với mô-đun hóa: implementation ẩn các phụ thuộc truyền khỏi người tiêu dùng mô-đun. Nếu mô-đun :profile sử dụng :networking qua implementation, người tiêu dùng của :profile không biết về :networking và không thể truy cập nó.
// settings.gradle — khai báo mô-đun
include ':app'
include ':feature:profile'
include ':feature:settings'
include ':core:network'
include ':core:database'
// build.gradle feature/profile — phụ thuộc mô-đun
dependencies {
implementation project(':core:network')
implementation project(':core:database')
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
}
Mã cho thấy cấu trúc của một dự án Android mô-đun. Settings.gradle liệt kê tất cả các mô-đun và build.gradle của mỗi mô-đun tính năng chỉ định rõ các mô-đun cốt lõi mà nó cần. Hệ thống xây dựng tự động giải quyết các phụ thuộc truyền và xây dựng các mô-đun theo đúng thứ tự.
Swift Package Manager (SPM) là công cụ mô-đun hóa tiêu chuẩn trên iOS từ năm 2019. SPM cho phép chia ứng dụng thành các Swift Packages, mỗi gói có thể là thư viện hoặc tệp thực thi. Một Package xác định các mô-đun (targets) và phụ thuộc của chúng thông qua Package.swift. SPM được tích hợp vào Xcode và không yêu cầu công cụ bổ sung.
CocoaPods vẫn là trình quản lý phụ thuộc chính cho các thư viện bên thứ ba. Podfile và Podspec xác định cấu trúc mô-đun và CocoaPods tạo ra một workspace với các dự án pod riêng biệt. Để mô-đun hóa dự án của riêng mình, các nhóm ngày càng chọn SPM vì nó được tích hợp trong Xcode và không yêu cầu cài đặt.
Trong mô-đun hóa iOS, kiểm soát truy cập đóng vai trò quan trọng: public, package, internal, fileprivate và private. Một mô-đun chỉ xuất bản các kiểu cần có thể truy cập được cho các mô-đun khác. Chi tiết triển khai nội bộ được ẩn sau các bổ từ internal và private. Điều này ngăn chặn các phụ thuộc ẩn giữa các mô-đun.
// Package.swift — cấu trúc mô-đun của dự án iOS
let package = Package(
name: "MyApp",
platforms: [.iOS(SupportedPlatform.iOSVersion.v17)],
products: [
.library(name: "ProfileFeature", targets: ["ProfileFeature"]),
.library(name: "NetworkCore", targets: ["NetworkCore"]),
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
],
targets: [
.target(name: "ProfileFeature", dependencies: ["NetworkCore"]),
.target(name: "NetworkCore", dependencies: ["Alamofire"]),
]
)
Package.swift khai báo hai sản phẩm thư viện: ProfileFeature và NetworkCore. ProfileFeature phụ thuộc vào NetworkCore nhưng không biết về sự tồn tại của Alamofire — nó được ẩn bên trong NetworkCore. Sự cô lập như vậy là ứng dụng trực tiếp của SoC ở cấp độ mô-đun: các thay đổi trong máy khách HTTP không yêu cầu biên dịch lại ProfileFeature.
Lợi ích chính của mô-đun hóa là tốc độ phát triển. Các nhóm làm việc song song trên các mô-đun khác nhau mà không xung đột mã. Đường ống CI/CD chỉ xây dựng các mô-đun đã thay đổi và chỉ chạy kiểm thử của chúng. Thời gian phản hồi giảm và tần suất phát hành tăng. Spotify, Uber và Airbnb đã công bố các nghiên cứu điển hình về việc chuyển đổi sang kiến trúc mô-đun với cải thiện số liệu gấp 2–3 lần.
Lợi ích thứ hai là cô lập lỗi. Một lỗi trong mô-đun Profile không ảnh hưởng đến mô-đun Payments nếu không có phụ thuộc trực tiếp giữa chúng. Điều này đặc biệt quan trọng trong các ứng dụng có chức năng rủi ro cao (thanh toán, dữ liệu y tế), nơi lỗi ở màn hình không liên quan không nên chặn phát hành chức năng quan trọng.
Thách thức chính là quản lý phụ thuộc. Với thiết kế kém, một đồ thị mô-đun xuất hiện nơi thay đổi một mô-đun gây ra xây dựng lại hàng loạt hàng chục mô-đun khác. Giải pháp là tuân theo quy tắc không chu trình: đồ thị phụ thuộc mô-đun phải là đồ thị có hướng không chu trình (DAG). Các công cụ như Gradle Module Graph Assert giúp phát hiện chu trình tại thời điểm xây dựng.
Thách thức thứ hai là tăng thời gian thiết lập ban đầu. Tạo kiến trúc mô-đun đòi hỏi nhiều thời gian hơn ở giai đoạn khởi tạo dự án. Các dự án nhỏ với 1–3 nhà phát triển có thể không hưởng lợi từ mô-đun hóa, dành thời gian duy trì ranh giới mô-đun mà không có nhu cầu thực sự về song song hóa. Giải pháp là bắt đầu với khối nguyên khối và trích xuất các mô-đun khi nhóm phát triển.
Cách tiếp cận feature-first nhóm các mô-đun theo chức năng: mỗi màn hình hoặc nhóm màn hình trở thành một mô-đun riêng biệt. Cách tiếp cận layer-first chia mã theo tiêu chí kỹ thuật: các mô-đun riêng cho UI, logic nghiệp vụ và dữ liệu. Trong thực tế, hầu hết các nhóm chọn feature-first với các mô-đun cốt lõi — điều này mang lại sự cô lập tốt hơn và điều hướng dự án rõ ràng.
Sự lựa chọn giữa các cách tiếp cận phụ thuộc vào quy mô nhóm và khả năng dự đoán chức năng. Nếu bạn biết chính xác màn hình nào sẽ có trong dự án, feature-first cho phép mỗi nhà phát triển chịu trách nhiệm về mô-đun của riêng họ. Nếu chức năng thay đổi thường xuyên và chồng chéo giữa các màn hình, layer-first cung cấp tính linh hoạt hơn trong việc tái sử dụng mã giữa các tính năng khác nhau.
Câu hỏi thường gặp
Số lượng tối ưu phụ thuộc vào quy mô dự án và nhóm. Cho một nhóm 5 người, 6–10 mô-đun là đủ. Cho 20+ nhà phát triển, 20–40 mô-đun. Nguyên tắc: một mô-đun phải đủ nhỏ để một nhà phát triển hiểu hoàn toàn và đủ lớn để không tạo ra mạng lưới phụ thuộc quá mức.
Mô-đun hóa đúng cách tăng tốc xây dựng nhờ biên dịch song song và bộ nhớ đệm. Nhưng số lượng mô-đun quá mức với phụ thuộc chặt chẽ làm chậm xây dựng — Gradle và Xcode mất thời gian giải quyết đồ thị. Chìa khóa để xây dựng nhanh là giảm thiểu phụ thuộc truyền và duy trì tính không chu trình.
Có, nhưng theo từng bước. Bắt đầu bằng cách trích xuất các mô-đun cốt lõi (mạng, cơ sở dữ liệu), sau đó trích xuất từng tính năng một. Sử dụng feature flags để kích hoạt mã mô-đun mới song song với mã nguyên khối cũ. Việc di chuyển hoàn toàn một ứng dụng lớn mất từ 3 đến 12 tháng.
Mô-đun là đơn vị biên dịch trong một ứng dụng duy nhất. Microservices là các tiến trình riêng biệt chạy trên các máy chủ khác nhau. Mô-đun chia mã, microservices chia runtime. Trong phát triển di động, thuật ngữ “microapps” thường được sử dụng như một dạng kết hợp: các mô-đun tính năng có thể chạy như ứng dụng độc lập.
Mỗi mô-đun có kiểm thử đơn vị riêng chạy độc lập. Kiểm thử tích hợp xác minh tương tác giữa các mô-đun. Kiểm thử UI bao phủ các mô-đun tính năng với dữ liệu giả. Kiến trúc mô-đun đơn giản hóa kiểm thử: giả lập phụ thuộc của mô-đun khác dễ dàng hơn giả lập một phần của khối nguyên khối.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm