ماژولار بودن اصلی است که بر اساس آن اپلیکیشن از ماژولهای مستقل تشکیل میشود که هر کدام مسئول یک قابلیت هستند. به گفته Android Developers، تقسیم به ماژولها کامپایل را با کامپایل موازی سرعت میبخشد و به تیمها اجازه میدهد روی بخشهای مختلف اپلیکیشن مستقل کار کنند. معماری ماژولار به استانداردی برای پروژههای بزرگ موبایل با دهها توسعهدهنده تبدیل شده است.
نکات کلیدی
ماژولار بودن روشی برای سازماندهی کد است که در آن اپلیکیشن از ماژولهای کموابسته تشکیل شده است که هر کدام از طریق یک رابط عمومی قابلیت مشخصی را ارائه میدهند. برخلاف معماری یکپارچه که همه کلاسها در یک پروژه قرار دارند، رویکرد ماژولار کد را به واحدهای کامپایل فیزیکی مستقل تقسیم میکند.
هدف اصلی ماژولار بودن مدیریت پیچیدگی است. توسعهدهنده میتواند روی یک ماژول تمرکز کند بدون اینکه کل پایگاه کد را در ذهن داشته باشد. هر ماژول حوزه مسئولیت خود را دارد و میتواند مستقل از بقیه توسعه، آزمایش و مستقر شود. این به ویژه در پروژههای با ۱۰+ توسعهدهنده ارزشمند است، زیرا کار موازی روی یکپارچه به تضادهای ادغام مکرر منجر میشود.
مهم است که ماژولار بودن را از معماری لایهای متمایز کنیم. لایهها (Presentation, Domain, Data) کد را بر اساس معیار فنی تقسیم میکنند، در حالی که ماژولها بر اساس معیار قابلیتی تقسیم میکنند. ماژول «پروفایل کاربر» میتواند لایههای خاص خود را درون خود داشته باشد. در عمل رویکرد ماژولار و معماری لایهای ترکیب میشوند: هر ماژول ساختار سهلایهای خود را دارد.
ماژولهای Feature — محبوبترین نوع ماژولها. هر صفحه یا گروهی از صفحات مرتبط در یک ماژول جداگانه قرار میگیرد: Onboarding, Profile, Settings, Feed. ماژول Feature شامل همه چیز لازم برای عملکرد قابلیت است: UI، منطق کسبوکار، لایه داده. مرزهای ماژول محافظت میشوند — سایر قابلیتها نمیتوانند به کلاسهای داخلی آن دسترسی پیدا کنند.
ماژولهای Core شامل زیرساخت مشترک هستند: کار با شبکه، پایگاه داده، تحلیل، سیستم طراحی. آنها به ماژولهای Feature وابسته نیستند، اما ماژولهای Feature به آنها وابسته هستند. چنین تقسیمبندی تضمین میکند که تغییر SDK تحلیل روی لایه شبکه تأثیر نخواهد گذاشت و بالعکس. ماژولهای Core بدون تکرار کد بین قابلیتها复用 میشوند.
ماژولهای Shared شامل کدی هستند که توسط چندین قابلیت استفاده میشود: مدلهای داده، ابزارها، ثابتها، Viewهای سفارشی. مشکل اصلی ماژولهای Shared خطر تبدیل شدن به انبار («misc module») است که در آن کدهای ناهمگون در طول زمان جمع میشوند. قانون: ماژول Shared باید موضوع مشخصی داشته باشد، مثلاً «shared-ui» یا «shared-models».
در Android ماژولهای Shared اغلب به کتابخانههایی با پیشوند lib تقسیم میشوند: lib-network, lib-database, lib-ui-components. در iOS همان وظایف توسط Swift Packages داخلی در Workspace انجام میشود. در عمل تیمها تعداد ماژولهای Shared را به ۳–۵ محدود میکنند تا شبکه وابستگی اضافی که کامپایل را پیچیده میکند ایجاد نشود.
ماژولهای تست جداگانه اجازه میدهند تستها فقط برای ماژول تغییر یافته اجرا شوند بدون اجرای کل پایگاه تست. این زمان pipeline CI/CD را از ساعتها به دقیقه کاهش میدهد. ماژولها تفکیک در سطح کامپایل را تضمین میکنند: ماژول لایه شبکه نمیتواند به طور تصادفی کتابخانههای UI را در تستها import کند.
هر ماژول باید API عمومی مشخصی داشته باشد. در Android این از طریق اصلاحکنندههای دسترسی و api vs implementation در Gradle حاصل میشود. در iOS — از طریق اصلاحکنندههای دسترسی public/internal و وابستگیهای مدیریتشده از طریق Package.swift. کاهش دید به حداقل لازم — یک روش کلیدی طراحی ماژولار است.
Gradle به صورت بومی از معماری ماژولار پشتیبانی میکند: هر ماژول یک واحد کامپایل جداگانه با فایل build.gradle خود است. پروژههای Android از ترکیب ماژول application (app) و چند ماژول library استفاده میکنند. ماژولهای کتابخانه نمیتوانند به عنوان اپلیکیشن اجرا شوند، اما میتوانند به عنوان AAR در مخزن منتشر شوند.
ویژگی کلیدی Gradle — کامپایل موازی ماژولهای مستقل. اگر ماژولهای A, B و C به یکدیگر وابسته نباشند، Gradle آنها را همزمان و با استفاده از همه هستههای پردازنده کامپایل میکند. در پروژههای با ۲۰+ ماژول این کار کامپایل کامل را از ۱۵ به ۳–۵ دقیقه کاهش میدهد. کامپایل افزایشی ماژول تغییر یافته ثانیهها طول میکشد.
Gradle دو نوع وابستگی بین ماژولها ارائه میدهد: api (متعدی) و implementation (غیرمتعدی). تفاوت برای ماژولار بودن حیاتی است: implementation وابستگیهای متعدی را از مصرفکنندگان ماژول پنهان میکند. اگر ماژول :profile از :networking از طریق implementation استفاده کند، مصرفکنندگان :profile از :networking اطلاع ندارند و نمیتوانند به آن مراجعه کنند.
// settings.gradle — اعلام ماژولها
include ':app'
include ':feature:profile'
include ':feature:settings'
include ':core:network'
include ':core:database'
// build.gradle feature/profile — وابستگیهای ماژول
dependencies {
implementation project(':core:network')
implementation project(':core:database')
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
}
کد ساختار پروژه Android ماژولار را نشان میدهد. Settings.gradle همه ماژولها را فهرست میکند و build.gradle هر ماژول Feature فقط ماژولهای Core مورد نیاز خود را مشخص میکند. سیستم کامپایل به طور خودکار وابستگیهای متعدی را حل کرده و ماژولها را به ترتیب صحیح میسازد.
Swift Package Manager (SPM) — ابزار استاندارد ماژولار بودن در iOS از سال ۲۰۱۹. SPM اجازه میدهد اپلیکیشن به Swift Packageهایی تقسیم شود که هر کدام میتوانند library یا executable باشند. Package ماژولها (targets) و وابستگیهای آنها را از طریق Package.swift تعریف میکند. SPM در Xcode یکپارچه شده و به ابزار اضافی نیاز ندارد.
CocoaPods مدیر اصلی وابستگی برای کتابخانههای شخص ثالث باقی میماند. Podfile و Podspec ساختار ماژولار را تعریف میکنند و CocoaPods workspace با پروژههای pod جداگانه تولید میکند. برای ماژولار بودن خود پروژه، تیمها به طور فزایندهای SPM را انتخاب میکنند زیرا در Xcode تعبیه شده است و نیاز به نصب ندارد.
در ماژولار بودن iOS کنترل دسترسی نقش مهمی ایفا میکند: public, package, internal, fileprivate و private. ماژول فقط انواعی را منتشر میکند که باید برای سایر ماژولها در دسترس باشند. جزئیات داخلی پیادهسازی پشت اصلاحکنندههای internal و private پنهان میشوند. این از ایجاد وابستگیهای پنهان بین ماژولها جلوگیری میکند.
// Package.swift — ساختار ماژولار پروژه 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 دو محصول کتابخانهای را اعلام میکند: ProfileFeature و NetworkCore. ProfileFeature به NetworkCore وابسته است، اما از وجود Alamofire اطلاعی ندارد — آن درون NetworkCore پنهان شده است. چنین ایزولهسازی اعمال مستقیم تفکیک در سطح ماژول است: تغییرات در HTTP client نیازی به کامپایل مجدد ProfileFeature ندارد.
مزیت اصلی ماژولار بودن سرعت توسعه است. تیمها به صورت موازی روی ماژولهای مختلف بدون تضاد در کد کار میکنند. Pipeline CI/CD فقط ماژولهای تغییر یافته را میسازد و فقط تستهای آنها را اجرا میکند. زمان بازخورد کاهش و دفعات انتشار افزایش مییابد. Spotify، Uber و Airbnb موارد مهاجرت به معماری ماژولار را با بهبود معیارها ۲–۳ برابر منتشر کردهاند.
مزیت دوم — ایزوله کردن خطاها. باگ در ماژول Profile بر ماژول Payments تأثیر نمیگذارد، اگر بین آنها وابستگی مستقیم وجود نداشته باشد. این به ویژه در اپلیکیشنهای با قابلیت پرخطر (پرداختها، دادههای پزشکی) مهم است که خطا در یک صفحه نامرتبط نباید انتشار قابلیت حیاتی را مسدود کند.
چالش اصلی — مدیریت وابستگیها. در طراحی نادرست، گرافی از ماژولها ایجاد میشود که تغییر یک ماژول دهها ماژول دیگر را به صورت آبشاری بازسازی میکند. راهحل — پیروی از قانون غیرچرخهای بودن: گراف وابستگی ماژولها باید یک گراف جهتدار غیرچرخهای (DAG) باشد. ابزارهایی مانند Gradle Module Graph Assert به کشف چرخهها در مرحله کامپایل کمک میکنند.
چالش دوم — افزایش زمان راهاندازی اولیه. ایجاد معماری ماژولار زمان بیشتری در مرحله اولیه پروژه نیاز دارد. پروژههای کوچک با ۱–۳ توسعهدهنده ممکن است از ماژولار بودن سود نبرند و وقت خود را برای حفظ مرزهای ماژول بدون نیاز واقعی به موازیسازی صرف کنند. راهحل — شروع با یکپارچه و جدا کردن ماژولها با رشد تیم.
رویکرد Feature-first ماژولها را بر اساس قابلیت گروهبندی میکند: هر صفحه یا گروهی از صفحات یک ماژول جداگانه میشود. رویکرد Layer-first کد را بر اساس معیار فنی تقسیم میکند: ماژولهای جداگانه برای UI، منطق کسبوکار و دادهها. در عمل بیشتر تیمها feature-first را با ماژولهای Core انتخاب میکنند — این ایزولهسازی بهتر و ناوبری واضح در پروژه را فراهم میکند.
انتخاب بین رویکردها به اندازه تیم و پیشبینیپذیری قابلیتها بستگی دارد. اگر دقیقاً میدانید چه صفحاتی در پروژه خواهند بود، feature-first به هر توسعهدهنده اجازه میدهد مسئول ماژول خود باشد. اگر قابلیتها اغلب تغییر میکنند و بین صفحات تداخل دارند، layer-first انعطافپذیری بیشتری در استفاده مجدد از کد بین قابلیتهای مختلف میدهد.
سوالات متداول
تعداد بهینه به اندازه پروژه و تیم بستگی دارد. برای تیم ۵ نفره ۶–۱۰ ماژول کافی است. برای ۲۰+ توسعهدهنده — ۲۰–۴۰ ماژول. قانون: ماژول باید به اندازه کافی کوچک باشد که یک توسعهدهنده آن را کاملاً درک کند و به اندازه کافی بزرگ باشد که شبکه وابستگی اضافی ایجاد نکند.
ماژولار بودن صحیح کامپایل را با کامپایل موازی و حافظهنهانسازی سرعت میبخشد. اما تعداد بیش از حد ماژولها با وابستگیهای متراکم کامپایل را کند میکند — Gradle و Xcode زمان را صرف حل گراف میکنند. کلید کامپایل سریع — به حداقل رساندن وابستگیهای متعدی و رعایت غیرچرخهای بودن.
بله، اما به صورت تدریجی. با جدا کردن ماژولهای Core (شبکه، پایگاه داده) شروع کنید، سپس قابلیتها را یکی یکی استخراج کنید. از feature flags برای فعال کردن کد ماژولار جدید به موازات کد یکپارچه قدیمی استفاده کنید. مهاجرت کامل یک اپلیکیشن بزرگ ۳ تا ۱۲ ماه طول میکشد.
ماژولها واحدهای کامپایل درون یک اپلیکیشن هستند. میکروسرویسها — فرآیندهای جداگانهای که روی سرورهای مختلف اجرا میشوند. ماژولها کد را تقسیم میکنند، میکروسرویسها زمان اجرا را تقسیم میکنند. در توسعه موبایل اغلب از اصطلاح «microapps» به عنوان ترکیبی استفاده میشود: ماژولهای Feature که میتوانند به عنوان اپلیکیشنهای مستقل اجرا شوند.
هر ماژول تستهای واحد خود را دارد که به صورت مستقل اجرا میشوند. تستهای یکپارچهسازی تعامل بین ماژولها را بررسی میکنند. تستهای UI ماژولهای Feature را با دادههای mock پوشش میدهند. معماری ماژولار تست را ساده میکند: mock کردن وابستگی یک ماژول دیگر آسانتر از mock کردن بخشی از یکپارچه است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید