هیچ استراتژی ماژولارسازی واحدی وجود ندارد که برای همه پروژهها مناسب باشد. با توجه به ماهیت انعطافپذیر Gradle، محدودیتهای کمی در مورد نحوه سازماندهی یک پروژه وجود دارد. این صفحه مروری بر برخی از قوانین کلی و الگوهای رایجی دارد که میتوانید هنگام توسعه برنامههای اندروید چند ماژوله از آنها استفاده کنید.
اصل انسجام بالا و اتصال کم
یکی از راههای توصیف یک کدبیس ماژولار، استفاده از ویژگیهای اتصال (coupling) و انسجام (cohesion) است. اتصال (coupling) میزان وابستگی ماژولها به یکدیگر را اندازهگیری میکند. در این زمینه، انسجام (cohesion)، میزان ارتباط عملکردی عناصر یک ماژول واحد را اندازهگیری میکند. به عنوان یک قاعده کلی، باید برای اتصال کم و انسجام بالا تلاش کنید:
- اتصال کم به این معنی است که ماژولها باید تا حد امکان از یکدیگر مستقل باشند، به طوری که تغییرات در یک ماژول هیچ تأثیری بر سایر ماژولها نداشته باشد یا تأثیر بسیار کمی داشته باشد. ماژولها نباید از عملکرد داخلی ماژولهای دیگر اطلاعی داشته باشند .
- انسجام بالا به این معنی است که ماژولها باید مجموعهای از کدها را تشکیل دهند که به عنوان یک سیستم عمل میکنند. آنها باید مسئولیتهای واضحی داشته باشند و در محدوده دانش دامنه خاصی باقی بمانند. یک برنامه کتاب الکترونیکی نمونه را در نظر بگیرید. ممکن است ترکیب کدهای مربوط به کتاب و پرداخت در یک ماژول نامناسب باشد زیرا آنها دو دامنه عملکردی متفاوت هستند.
انواع ماژولها
نحوه سازماندهی ماژولهای شما عمدتاً به معماری برنامه شما بستگی دارد. در زیر برخی از انواع رایج ماژولهایی که میتوانید در برنامه خود با پیروی از معماری برنامه پیشنهادی ما معرفی کنید، آورده شده است.
ماژولهای داده
یک ماژول داده معمولاً شامل یک مخزن، منابع داده و کلاسهای مدل است. سه مسئولیت اصلی یک ماژول داده عبارتند از:
- کپسولهسازی تمام دادهها و منطق کسبوکار یک دامنه خاص : هر ماژول داده باید مسئول مدیریت دادههایی باشد که نشاندهنده یک دامنه خاص هستند. این ماژول میتواند انواع مختلفی از دادهها را تا زمانی که مرتبط باشند، مدیریت کند.
- مخزن را به عنوان یک API خارجی نمایش دهید : API عمومی یک ماژول داده باید یک مخزن باشد زیرا آنها مسئول نمایش دادهها به بقیه برنامه هستند.
- پنهان کردن تمام جزئیات پیادهسازی و منابع داده از بیرون : منابع داده فقط باید توسط مخازن همان ماژول قابل دسترسی باشند. آنها از بیرون پنهان میمانند. میتوانید این کار را با استفاده از کلمه کلیدی
privateیاinternalvisibility در کاتلین انجام دهید.

ماژولهای ویژگی
یک ویژگی، بخش جداگانهای از عملکرد یک برنامه است که معمولاً با یک صفحه یا مجموعهای از صفحات مرتبط، مانند جریان ثبت نام یا پرداخت، مطابقت دارد. اگر برنامه شما دارای نوار ناوبری پایین صفحه است، احتمالاً هر مقصد یک ویژگی است.

ویژگیها با صفحه نمایشها یا مقاصد در برنامه شما مرتبط هستند. بنابراین، احتمالاً یک رابط کاربری (UI) و ViewModel مرتبط برای مدیریت منطق و وضعیت خود دارند. یک ویژگی واحد لزوماً نباید به یک نمای واحد یا مقصد ناوبری محدود شود. ماژولهای ویژگی به ماژولهای داده وابسته هستند.

ماژولهای برنامه
ماژولهای برنامه، نقطه ورود به برنامه هستند. آنها به ماژولهای ویژگی وابسته هستند و معمولاً ناوبری ریشه را فراهم میکنند. به لطف build variants، یک ماژول برنامه میتواند به چندین فایل باینری مختلف کامپایل شود.

اگر برنامه شما برای چندین نوع دستگاه مانند Android Auto، Wear یا TV طراحی شده است، برای هر کدام یک ماژول برنامه تعریف کنید. این کار به جداسازی وابستگیهای خاص پلتفرم کمک میکند.

ماژولهای رایج
ماژولهای رایج، که به عنوان ماژولهای اصلی نیز شناخته میشوند، حاوی کدی هستند که سایر ماژولها مرتباً از آن استفاده میکنند. آنها افزونگی را کاهش میدهند و هیچ لایه خاصی را در معماری برنامه نشان نمیدهند. در زیر نمونههایی از ماژولهای رایج آمده است:
- ماژول رابط کاربری : اگر از عناصر رابط کاربری سفارشی یا برندسازی پیچیده در برنامه خود استفاده میکنید، باید کپسولهسازی مجموعه ویجتهای خود را در یک ماژول برای استفاده مجدد از همه ویژگیها در نظر بگیرید. این میتواند به ایجاد سازگاری رابط کاربری شما در بین ویژگیهای مختلف کمک کند. به عنوان مثال، اگر قالببندی شما متمرکز باشد، میتوانید از یک بازسازی دردناک هنگام تغییر برند جلوگیری کنید.
- ماژول تجزیه و تحلیل : ردیابی اغلب توسط الزامات تجاری و با توجه کم به معماری نرمافزار تعیین میشود. ردیابهای تجزیه و تحلیل اغلب در بسیاری از اجزای نامرتبط استفاده میشوند. اگر این مورد برای شما صدق میکند، داشتن یک ماژول تجزیه و تحلیل اختصاصی میتواند ایده خوبی باشد.
- ماژول شبکه : وقتی ماژولهای زیادی به اتصال شبکه نیاز دارند، میتوانید ماژولی را در نظر بگیرید که به ارائه یک کلاینت http اختصاص داده شده باشد. این امر به ویژه زمانی مفید است که کلاینت شما نیاز به پیکربندی سفارشی داشته باشد.
- ماژول کاربردی : ابزارهای کاربردی که به عنوان کمککنندهها نیز شناخته میشوند، معمولاً قطعات کوچکی از کد هستند که در سراسر برنامه مورد استفاده مجدد قرار میگیرند. نمونههایی از ابزارها شامل کمککنندههای تست، یک تابع قالببندی ارز، اعتبارسنج ایمیل یا یک عملگر سفارشی هستند.
ماژولهای تست
ماژولهای تست، ماژولهای اندروید هستند که فقط برای اهداف تست استفاده میشوند. این ماژولها شامل کد تست، منابع تست و وابستگیهای تست هستند که فقط برای اجرای تستها مورد نیاز هستند و در طول زمان اجرای برنامه نیازی به آنها نیست. ماژولهای تست برای جدا کردن کد مخصوص تست از برنامه اصلی ایجاد میشوند و مدیریت و نگهداری کد ماژول را آسانتر میکنند.
موارد استفاده برای ماژولهای آزمایشی
مثالهای زیر موقعیتهایی را نشان میدهند که پیادهسازی ماژولهای تست میتواند بهطور ویژه مفید باشد:
کد تست مشترک : اگر چندین ماژول در پروژه خود دارید و برخی از کدهای تست برای بیش از یک ماژول قابل استفاده هستند، میتوانید یک ماژول تست برای اشتراکگذاری کد ایجاد کنید. این میتواند به کاهش تکرار کمک کند و نگهداری کد تست شما را آسانتر کند. کد تست مشترک میتواند شامل کلاسها یا توابع کاربردی، مانند assertionهای سفارشی یا matcherها، و همچنین دادههای تست، مانند پاسخهای شبیهسازی شده JSON باشد.
پیکربندیهای ساخت تمیزتر : ماژولهای آزمایشی به شما این امکان را میدهند که پیکربندیهای ساخت تمیزتری داشته باشید، زیرا میتوانند فایل
build.gradleمخصوص به خود را داشته باشند. لازم نیست فایلbuild.gradleماژول برنامه خود را با پیکربندیهایی که فقط برای آزمایشها مرتبط هستند، شلوغ کنید.تستهای یکپارچهسازی : ماژولهای تست میتوانند برای ذخیره تستهای یکپارچهسازی که برای آزمایش تعاملات بین بخشهای مختلف برنامه شما، از جمله رابط کاربری، منطق کسبوکار، درخواستهای شبکه و پرسوجوهای پایگاه داده استفاده میشوند، استفاده شوند.
برنامههای کاربردی در مقیاس بزرگ : ماژولهای تست به ویژه برای برنامههای کاربردی در مقیاس بزرگ با پایگاههای کد پیچیده و چندین ماژول مفید هستند. در چنین مواردی، ماژولهای تست میتوانند به بهبود سازماندهی کد و قابلیت نگهداری آن کمک کنند.

ارتباط ماژول به ماژول
ماژولها به ندرت کاملاً جدا از هم وجود دارند و اغلب به ماژولهای دیگر متکی هستند و با آنها ارتباط برقرار میکنند. حتی زمانی که ماژولها با هم کار میکنند و مرتباً اطلاعات را رد و بدل میکنند، پایین نگه داشتن اتصال مهم است. گاهی اوقات ارتباط مستقیم بین دو ماژول یا مطلوب نیست، مانند محدودیتهای معماری. همچنین ممکن است غیرممکن باشد، مانند وابستگیهای چرخهای.

برای غلبه بر این مشکل، میتوانید یک ماژول سوم داشته باشید که بین دو ماژول دیگر واسطهگری کند . ماژول میانجی میتواند به پیامهای هر دو ماژول گوش دهد و در صورت نیاز آنها را ارسال کند. در برنامه نمونه ما، صفحه پرداخت باید بداند کدام کتاب را باید خریداری کند، حتی اگر این رویداد در یک صفحه جداگانه که بخشی از یک ویژگی متفاوت است، آغاز شده باشد. در این مورد، میانجی ماژولی است که نمودار ناوبری (معمولاً یک ماژول برنامه) را در اختیار دارد. در این مثال، ما از ناوبری برای انتقال دادهها از ویژگی خانه به ویژگی پرداخت با استفاده از مؤلفه ناوبری استفاده میکنیم.
navController.navigate("checkout/$bookId")
مقصد پرداخت، یک شناسه کتاب را به عنوان آرگومان دریافت میکند که از آن برای دریافت اطلاعات مربوط به کتاب استفاده میکند. میتوانید از شناسه وضعیت ذخیره شده برای بازیابی آرگومانهای ناوبری درون ViewModel یک ویژگی مقصد استفاده کنید.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
شما نباید اشیاء را به عنوان آرگومانهای ناوبری ارسال کنید. در عوض، از شناسههای سادهای استفاده کنید که ویژگیها میتوانند برای دسترسی و بارگذاری منابع مورد نظر از لایه داده از آنها استفاده کنند. به این ترتیب، اتصال را پایین نگه میدارید و اصل منبع واحد حقیقت را نقض نمیکنید.
در مثال زیر، هر دو ماژول ویژگی به یک ماژول داده وابسته هستند. این امر باعث میشود مقدار دادهای که ماژول واسطه باید ارسال کند به حداقل برسد و اتصال بین ماژولها کم بماند. به جای ارسال اشیاء، ماژولها باید شناسههای اولیه را مبادله کرده و منابع را از یک ماژول داده مشترک بارگذاری کنند.

وارونگی وابستگی
وارونگی وابستگی زمانی است که شما کد خود را طوری سازماندهی میکنید که انتزاع از پیادهسازی ملموس جدا باشد.
- انتزاع : قراردادی که نحوه تعامل اجزا یا ماژولهای برنامه شما با یکدیگر را تعریف میکند. ماژولهای انتزاع، API سیستم شما را تعریف میکنند و شامل رابطها و مدلها هستند.
- پیادهسازی عینی : ماژولهایی که به ماژول انتزاعی وابسته هستند و رفتار یک انتزاع را پیادهسازی میکنند.
ماژولهایی که به رفتار تعریفشده در ماژول انتزاع وابسته هستند، باید فقط به خود انتزاع وابسته باشند، نه به پیادهسازیهای خاص.

مثال
یک ماژول feature را تصور کنید که برای کار کردن به یک پایگاه داده نیاز دارد. ماژول feature کاری به نحوه پیادهسازی پایگاه داده، چه یک پایگاه داده محلی Room و چه یک نمونه Firestore از راه دور، ندارد. فقط نیاز به ذخیره و خواندن دادههای برنامه دارد.
برای دستیابی به این هدف، ماژول ویژگی به جای یک پیادهسازی خاص پایگاه داده، به ماژول انتزاع وابسته است. این انتزاع، API پایگاه داده برنامه را تعریف میکند. به عبارت دیگر، قوانینی را برای نحوه تعامل با پایگاه داده تعیین میکند. این امر به ماژول ویژگی اجازه میدهد تا از هر پایگاه دادهای بدون نیاز به دانستن جزئیات پیادهسازی زیربنایی آن استفاده کند.
ماژول پیادهسازی عینی، پیادهسازی واقعی APIهای تعریفشده در ماژول انتزاع را فراهم میکند. برای انجام این کار، ماژول پیادهسازی به ماژول انتزاع نیز وابسته است.
تزریق وابستگی
شاید تا الان از خودتان پرسیده باشید که ماژول feature چگونه به ماژول implementation متصل میشود. پاسخ، تزریق وابستگی (Dependency Injection) است. ماژول feature مستقیماً نمونه پایگاه داده مورد نیاز را ایجاد نمیکند. در عوض، وابستگیهای مورد نیاز خود را مشخص میکند. این وابستگیها سپس به صورت خارجی، معمولاً در ماژول app ، ارائه میشوند.
releaseImplementation(project(":database:impl:firestore"))
debugImplementation(project(":database:impl:room"))
androidTestImplementation(project(":database:impl:mock"))
مزایا
مزایای جداسازی APIها و پیادهسازیهای آنها به شرح زیر است:
- قابلیت تعویض : با جداسازی واضح ماژولهای API و پیادهسازی، میتوانید چندین پیادهسازی برای یک API مشابه توسعه دهید و بدون تغییر کدی که از API استفاده میکند، بین آنها جابجا شوید. این امر میتواند به ویژه در سناریوهایی که میخواهید قابلیتها یا رفتارهای مختلفی را در زمینههای مختلف ارائه دهید، مفید باشد. به عنوان مثال، یک پیادهسازی آزمایشی برای آزمایش در مقابل یک پیادهسازی واقعی برای تولید.
- جداسازی : جداسازی به این معنی است که ماژولهایی که از انتزاعها استفاده میکنند به هیچ فناوری خاصی وابسته نیستند. اگر بعداً تصمیم بگیرید پایگاه داده خود را از Room به Firestore تغییر دهید، آسانتر خواهد بود زیرا تغییرات فقط در ماژول خاصی که کار را انجام میدهد (ماژول پیادهسازی) اتفاق میافتد و بر سایر ماژولهایی که از API پایگاه داده شما استفاده میکنند، تأثیری نخواهد گذاشت.
- قابلیت آزمایش : جداسازی APIها از پیادهسازیهایشان میتواند آزمایش را تا حد زیادی تسهیل کند. میتوانید موارد آزمایشی را برای قراردادهای API بنویسید. همچنین میتوانید از پیادهسازیهای مختلف برای آزمایش سناریوها و موارد حاشیهای مختلف، از جمله پیادهسازیهای آزمایشی، استفاده کنید.
- بهبود عملکرد ساخت : وقتی یک API و پیادهسازی آن را به ماژولهای مختلف تقسیم میکنید، تغییرات در ماژول پیادهسازی، سیستم ساخت را مجبور به کامپایل مجدد ماژولها بسته به ماژول API نمیکند. این امر منجر به زمان ساخت سریعتر و افزایش بهرهوری میشود، به ویژه در پروژههای بزرگ که زمان ساخت میتواند قابل توجه باشد.
چه زمانی جدا شویم
در موارد زیر، جداسازی APIها از پیادهسازیهایشان مفید است:
- قابلیتهای متنوع : اگر بتوانید بخشهایی از سیستم خود را به روشهای مختلف پیادهسازی کنید، یک API شفاف امکان تعویض پیادهسازیهای مختلف را فراهم میکند. به عنوان مثال، ممکن است یک سیستم رندر داشته باشید که از OpenGL یا Vulkan استفاده میکند، یا یک سیستم صورتحساب که با Play یا API صورتحساب داخلی شما کار میکند.
- برنامههای چندگانه : اگر در حال توسعه چندین برنامه با قابلیتهای مشترک برای پلتفرمهای مختلف هستید، میتوانید APIهای مشترکی تعریف کنید و پیادهسازیهای خاصی را برای هر پلتفرم توسعه دهید.
- تیمهای مستقل : این جداسازی به توسعهدهندگان یا تیمهای مختلف اجازه میدهد تا بهطور همزمان روی بخشهای مختلف کدبیس کار کنند. توسعهدهندگان باید بر درک قراردادهای API و استفاده صحیح از آنها تمرکز کنند. آنها نیازی به نگرانی در مورد جزئیات پیادهسازی سایر ماژولها ندارند.
- کدبیس بزرگ : وقتی کدبیس بزرگ یا پیچیده است، جداسازی API از پیادهسازی، کد را قابل مدیریتتر میکند. این به شما امکان میدهد کدبیس را به واحدهای جزئیتر، قابل فهمتر و قابل نگهداریتر تقسیم کنید.
چگونه پیاده سازی کنیم؟
برای پیادهسازی وارونگی وابستگی، مراحل زیر را دنبال کنید:
- ایجاد یک ماژول انتزاعی : این ماژول باید شامل APIهایی (رابطها و مدلها) باشد که رفتار ویژگی شما را تعریف میکنند.
- ایجاد ماژولهای پیادهسازی : ماژولهای پیادهسازی باید به ماژول API متکی باشند و رفتار یک انتزاع را پیادهسازی کنند.

شکل 10. ماژولهای پیادهسازی به ماژول انتزاع وابسته هستند. - ماژولهای سطح بالا را به ماژولهای انتزاعی وابسته کنید : به جای وابستگی مستقیم به یک پیادهسازی خاص، ماژولهای خود را به ماژولهای انتزاعی وابسته کنید. ماژولهای سطح بالا نیازی به دانستن جزئیات پیادهسازی ندارند، آنها فقط به قرارداد (API) نیاز دارند.

شکل ۱۱. ماژولهای سطح بالا به انتزاعات وابستهاند، نه پیادهسازی. - ماژول پیادهسازی را ارائه دهید : در نهایت، باید پیادهسازی واقعی وابستگیهای خود را ارائه دهید. پیادهسازی خاص به تنظیمات پروژه شما بستگی دارد، اما ماژول app معمولاً جای خوبی برای انجام این کار است. برای ارائه پیادهسازی، آن را به عنوان یک وابستگی برای نوع ساخت انتخابی یا یک مجموعه منبع آزمایشی مشخص کنید.

شکل ۱۲. ماژول App پیادهسازی واقعی را ارائه میدهد.
بهترین شیوههای عمومی
همانطور که در ابتدا اشاره شد، هیچ راه درست و واحدی برای توسعه یک برنامه چند ماژوله وجود ندارد. درست مانند معماریهای نرمافزاری متعدد، روشهای بیشماری برای ماژولار کردن یک برنامه وجود دارد. با این وجود، توصیههای کلی زیر میتواند به شما کمک کند تا کد خود را خواناتر، قابل نگهداریتر و قابل آزمایشتر کنید.
پیکربندی خود را ثابت نگه دارید
هر ماژول سربار پیکربندی را ایجاد میکند. اگر تعداد ماژولهای شما به یک آستانه خاص برسد، مدیریت پیکربندی سازگار به یک چالش تبدیل میشود. به عنوان مثال، مهم است که ماژولها از وابستگیهای یک نسخه استفاده کنند. اگر نیاز دارید تعداد زیادی ماژول را فقط برای تغییر نسخه وابستگی بهروزرسانی کنید، این کار نه تنها یک تلاش است، بلکه زمینهای برای اشتباهات احتمالی نیز فراهم میکند. برای حل این مشکل، میتوانید از یکی از ابزارهای gradle برای متمرکز کردن پیکربندی خود استفاده کنید:
- کاتالوگهای نسخه، فهرستی از وابستگیهای ایمن از نظر نوع هستند که توسط Gradle در حین همگامسازی ایجاد میشوند. این یک مکان مرکزی برای اعلام تمام وابستگیهای شما است و برای همه ماژولهای یک پروژه در دسترس است.
- از افزونههای قراردادی برای اشتراکگذاری منطق ساخت بین ماژولها استفاده کنید.
تا حد امکان کمتر در معرض دید قرار دهید
رابط عمومی یک ماژول باید حداقل باشد و فقط موارد ضروری را در معرض نمایش قرار دهد. نباید هیچ جزئیات پیادهسازی به بیرون درز کند. همه چیز را تا حد امکان در محدوده دید قرار دهید. از محدوده دید private یا internal کاتلین برای تعریف module-private استفاده کنید. هنگام تعریف وابستگیها در ماژول خود، implementation به api ترجیح دهید. مورد دوم وابستگیهای انتقالی را در معرض دید مصرفکنندگان ماژول شما قرار میدهد. استفاده از پیادهسازی ممکن است زمان ساخت را بهبود بخشد زیرا تعداد ماژولهایی را که نیاز به بازسازی دارند کاهش میدهد.
ماژولهای کاتلین و جاوا را ترجیح میدهند
سه نوع ماژول اساسی وجود دارد که اندروید استودیو از آنها پشتیبانی میکند:
- ماژولهای برنامه، نقطه ورود به برنامه شما هستند. آنها میتوانند شامل کد منبع، منابع، داراییها و یک
AndroidManifest.xmlباشند. خروجی یک ماژول برنامه، یک بسته برنامه اندروید (AAB) یا یک بسته برنامه اندروید (APK) است. - ماژولهای کتابخانه محتوای مشابهی با ماژولهای برنامه دارند. آنها توسط سایر ماژولهای اندروید به عنوان یک وابستگی استفاده میشوند. خروجی یک ماژول کتابخانه یک بایگانی اندروید (AAR) است که از نظر ساختاری با ماژولهای برنامه یکسان است، اما در یک فایل بایگانی اندروید (AAR) کامپایل میشوند که بعداً میتواند توسط سایر ماژولها به عنوان یک وابستگی استفاده شود. یک ماژول کتابخانه امکان کپسولهسازی و استفاده مجدد از همان منطق و منابع را در بسیاری از ماژولهای برنامه فراهم میکند.
- کتابخانههای کاتلین و جاوا هیچ منبع، دارایی یا فایل مانیفست اندروید ندارند.
از آنجایی که ماژولهای اندروید دارای سربار هستند، ترجیحاً تا حد امکان از نوع کاتلین یا جاوا استفاده کنید.