תבניות מודולריזציה נפוצות

אין אסטרטגיית מודולריזציה אחת שמתאימה לכל הפרויקטים. בגלל הגמישות של Gradle, יש מעט מגבלות על האופן שבו אפשר לארגן פרויקט. בדף הזה מפורטות כמה הנחיות כלליות ודפוסים נפוצים שאפשר להשתמש בהם כשמפתחים אפליקציות Android מרובות מודולים.

העיקרון של לכידות גבוהה וצימוד נמוך

אחת הדרכים לאפיין בסיס קוד מודולרי היא באמצעות המאפיינים צימוד ולכידות. הצימוד מודד את המידה שבה מודולים תלויים זה בזה. במקרה הזה, הלכידות היא מדד של הקשר הפונקציונלי בין האלמנטים של מודול יחיד. ככלל, מומלץ לשאוף לצימוד נמוך וללכידות גבוהה:

  • צימוד חלש פירושו שהמודולים צריכים להיות עצמאיים ככל האפשר זה מזה, כך ששינויים במודול אחד ישפיעו באופן מינימלי על מודולים אחרים או לא ישפיעו עליהם בכלל. למודולים לא אמור להיות ידע על הפעולות הפנימיות של מודולים אחרים.
  • לכידות גבוהה פירושה שהמודולים צריכים לכלול אוסף של קוד שפועל כמערכת. צריכות להיות להן הגדרות ברורות של תחומי האחריות, והן צריכות לפעול במסגרת הידע שלהן בתחום מסוים. כדאי לשקול להשתמש באפליקציה לדוגמה של ספר דיגיטלי. יכול להיות שלא מתאים לערב קוד שקשור להזמנה וקוד שקשור לתשלום באותו מודול, כי אלה שני תחומים פונקציונליים שונים.

סוגי מודולים

הדרך שבה מארגנים את המודולים תלויה בעיקר בארכיטקטורה של האפליקציה. בהמשך מוצגים כמה סוגים נפוצים של מודולים שאפשר להוסיף לאפליקציה כשפועלים לפי ארכיטקטורת האפליקציה המומלצת שלנו.

מודולים של נתונים

מודול נתונים בדרך כלל מכיל מאגר, מקורות נתונים ומחלקות מודלים. שלושת התפקידים העיקריים של מודול נתונים הם:

  1. הצפנה של כל הנתונים והלוגיקה העסקית של דומיין מסוים: כל מודול נתונים צריך להיות אחראי לטיפול בנתונים שמייצגים דומיין מסוים. הוא יכול להתמודד עם סוגים רבים של נתונים, כל עוד הם קשורים זה לזה.
  2. חשיפת המאגר כ-API חיצוני: ה-API הציבורי של מודול נתונים צריך להיות מאגר, כי הוא אחראי לחשיפת הנתונים לשאר האפליקציה.
  3. הסתרת כל פרטי ההטמעה ומקורות הנתונים מהחוץ: אפשר לגשת למקורות נתונים רק דרך מאגרי מידע מאותו מודול. הם נשארים מוסתרים מהעולם החיצוני. אפשר לאכוף את זה באמצעות מילת המפתח private או internal של Kotlin.
איור 1. מודולים של נתונים לדוגמה והתוכן שלהם.

מודולים של תכונות

תכונה היא חלק מבודד מהפונקציונליות של האפליקציה, שבדרך כלל תואם למסך או לסדרה של מסכים שקשורים זה לזה, כמו תהליך הרשמה או תהליך תשלום. אם באפליקציה יש סרגל ניווט בחלק התחתון, סביר להניח שכל יעד הוא תכונה.

איור 2. כל כרטיסייה באפליקציה הזו יכולה להיות מוגדרת כתכונה.

התכונות משויכות למסכים או ליעדים באפליקציה. לכן, סביר להניח שיש להן ממשק משתמש משויך ושהן ViewModel מטפלות בלוגיקה ובמצב שלהן. תכונה בודדת לא חייבת להיות מוגבלת לתצוגה בודדת או ליעד ניווט בודד. מודולים של תכונות תלויים במודולים של נתונים.

איור 3. מודולים לדוגמה של תכונות והתוכן שלהם.

מודולים של אפליקציות

מודולים של אפליקציות הם נקודת כניסה לאפליקציה. הם תלויים במודולים של התכונות, ובדרך כלל מספקים ניווט בסיסי. אפשר לקמפל מודול אפליקציה יחיד למספר קבצים בינאריים שונים, הודות לוריאציות של build.

איור 4. *הדגמה* ו *מלא* של גרסאות מוצר של גרף תלות במודולים.

אם האפליקציה מיועדת לכמה סוגי מכשירים, כמו Android Auto, ‏ Wear או TV, צריך להגדיר מודול אפליקציה לכל אחד מהם. כך אפשר להפריד בין תלות ספציפית בפלטפורמה.

איור 5. גרף התלות של אפליקציית Android Auto.

מודולים נפוצים

מודולים נפוצים, שנקראים גם מודולי ליבה, מכילים קוד שמודולים אחרים משתמשים בו לעיתים קרובות. הם מצמצמים את הכפילות ולא מייצגים שכבה ספציפית בארכיטקטורה של האפליקציה. דוגמאות למודולים נפוצים:

  • מודול ממשק משתמש: אם אתם משתמשים ברכיבי ממשק משתמש בהתאמה אישית או במיתוג מורכב באפליקציה, כדאי לשקול להשתמש באוסף הווידג'טים במודול כדי שתוכלו לעשות שימוש חוזר בכל התכונות. כך תוכלו לשמור על עקביות בממשק המשתמש שלכם בתכונות שונות. לדוגמה, אם העיצוב שלכם מרוכז, תוכלו להימנע משינוי מבנה מורכב ומייגע כשמתבצע מיתוג מחדש.
  • מודול Analytics: לעיתים קרובות המעקב מוכתב על ידי דרישות עסקיות, בלי להתחשב במבנה התוכנה. לעתים קרובות נעשה שימוש בכלי מעקב של Analytics בהרבה רכיבים לא קשורים. אם זה המצב אצלכם, כדאי להשתמש במודול ייעודי לניתוח נתונים.
  • מודול רשת: אם הרבה מודולים דורשים חיבור לרשת, כדאי לשקול להשתמש במודול ייעודי שיספק לקוח http. האפשרות הזו שימושית במיוחד אם הלקוח דורש הגדרה בהתאמה אישית.
  • מודול כלי עזר: כלי עזר, שנקראים גם רכיבי עזר, הם בדרך כלל קטעי קוד קטנים שנעשה בהם שימוש חוזר באפליקציה. דוגמאות לכלי עזר: כלי עזר לבדיקה, פונקציה לעיצוב מטבע, כלי לאימות כתובות אימייל או אופרטור בהתאמה אישית.

מודולים לבדיקה

מודולי בדיקה הם מודולים של Android שמשמשים למטרות בדיקה בלבד. המודולים מכילים קוד בדיקה, משאבי בדיקה ותלות בבדיקה שנדרשים רק להרצת בדיקות ולא נחוצים במהלך זמן הריצה של האפליקציה. מודולי בדיקה נוצרים כדי להפריד קוד ספציפי לבדיקה מהאפליקציה הראשית, וכך קל יותר לנהל ולתחזק את קוד המודול.

תרחישים לדוגמה לשימוש במודולים לבדיקה

בדוגמאות הבאות מוצגים מצבים שבהם הטמעה של מודולי בדיקה יכולה להיות מועילה במיוחד:

  • קוד בדיקה משותף: אם יש לכם כמה מודולים בפרויקט וחלק מקוד הבדיקה רלוונטי ליותר ממודול אחד, אתם יכולים ליצור מודול בדיקה כדי לשתף את הקוד. כך אפשר לצמצם את הכפילויות ולשמור על קוד הבדיקה בצורה קלה יותר. קוד בדיקה משותף יכול לכלול מחלקות או פונקציות של כלי עזר, כמו טענות או התאמות מותאמות אישית, וגם נתוני בדיקה, כמו תגובות JSON מדומה.

  • הגדרות build נקיות יותר: מודולי בדיקה מאפשרים לכם להגדיר build נקי יותר, כי יכול להיות להם קובץ build.gradle משלהם. אתם לא צריכים להוסיף לקובץ build.gradle של מודול האפליקציה הגדרות שרלוונטיות רק לבדיקות.

  • בדיקות שילוב: אפשר להשתמש במודולים של בדיקות כדי לאחסן בדיקות שילוב שמשמשות לבדיקת אינטראקציות בין חלקים שונים באפליקציה, כולל ממשק משתמש, לוגיקה עסקית, בקשות רשת ושאילתות במסד נתונים.

  • אפליקציות רחבות היקף: מודולי בדיקה שימושיים במיוחד לאפליקציות רחבות היקף עם בסיסי קוד מורכבים ומודולים רבים. במקרים כאלה, מודולי בדיקה יכולים לעזור לשפר את ארגון הקוד ואת יכולת התחזוקה שלו.

איור 6. אפשר להשתמש במודולים לבדיקה כדי לבודד מודולים שאחרת היו תלויים זה בזה.

תקשורת בין מודולים

מודולים כמעט אף פעם לא פועלים בנפרד לגמרי, ולרוב הם מסתמכים על מודולים אחרים ומתקשרים איתם. חשוב לשמור על צימוד נמוך גם כשמודולים עובדים יחד ומחליפים מידע בתדירות גבוהה. לפעמים לא רצוי שתהיה תקשורת ישירה בין שני מודולים, למשל במקרה של מגבלות ארכיטקטוניות. יכול להיות גם שזה בלתי אפשרי, למשל במקרה של תלות מחזורית.

איור 7. בגלל תלות מחזורית, אי אפשר ליצור תקשורת ישירה דו-כיוונית בין מודולים. נדרש מודול מתווך כדי לתאם את זרימת הנתונים בין שני מודולים עצמאיים אחרים.

כדי לפתור את הבעיה הזו, אפשר להשתמש במודול שלישי מתווך בין שני מודולים אחרים. מודול המתווך יכול להאזין להודעות משני המודולים ולהעביר אותן לפי הצורך. באפליקציה לדוגמה שלנו, המסך של תהליך התשלום צריך לדעת איזה ספר לרכוש, למרות שהאירוע נוצר במסך נפרד שהוא חלק מתכונה אחרת. במקרה הזה, מודול המתווך הוא המודול שבבעלותו גרף הניווט (בדרך כלל מודול אפליקציה). בדוגמה הזו, אנחנו משתמשים בניווט כדי להעביר את הנתונים מהתכונה הראשית לתכונת התשלום באמצעות הרכיב Navigation.

navController.navigate("checkout/$bookId")

יעד התשלום מקבל מזהה ספר כארגומנט, ומשתמש בו כדי לאחזר מידע על הספר. אפשר להשתמש בsaved state handle כדי לאחזר ארגומנטים של ניווט בתוך ViewModel של תכונת יעד.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, ) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      
}

אסור להעביר אובייקטים כארגומנטים של ניווט. במקום זאת, משתמשים במזהים פשוטים שהתכונות יכולות להשתמש בהם כדי לגשת למשאבים הרצויים משכבת הנתונים ולטעון אותם. כך, רמת הצימוד נשארת נמוכה ולא מופר העיקרון של מקור מרוכז אחד.

בדוגמה שלמטה, שני מודולי התכונות תלויים באותו מודול נתונים. כך אפשר לצמצם את כמות הנתונים שמודול הגישור צריך להעביר, ולשמור על צימוד נמוך בין המודולים. במקום להעביר אובייקטים, המודולים צריכים להחליף מזהים פרימיטיביים ולטעון את המקורות ממודול נתונים משותף.

איור 8. שני מודולים של תכונות שמסתמכים על מודול נתונים משותף.

היפוך תלות

היפוך תלות מתרחש כשמסדרים את הקוד כך שההפשטה מופרדת מהטמעה קונקרטית.

  • הפשטה (Abstraction): חוזה שמגדיר איך רכיבים או מודולים באפליקציה יוצרים אינטראקציה זה עם זה. מודולים של הפשטה מגדירים את ה-API של המערכת ומכילים ממשקים ומודלים.
  • הטמעה קונקרטית: מודולים שתלויים במודול ההפשטה ומטמיעים את ההתנהגות של ההפשטה.

מודולים שמסתמכים על ההתנהגות שמוגדרת במודול ההפשטה צריכים להסתמך רק על ההפשטה עצמה, ולא על ההטמעות הספציפיות.

איור 9. במקום שמודולים ברמה גבוהה יהיו תלויים ישירות במודולים ברמה נמוכה, מודולים ברמה גבוהה ומודולים להטמעה תלויים במודול ההפשטה.

דוגמה

תארו לעצמכם מודול תכונות שצריך מסד נתונים כדי לפעול. מודול התכונה לא מתייחס לאופן ההטמעה של מסד הנתונים, בין אם מדובר במסד נתונים מקומי של Room או במופע מרוחק של Firestore. היא צריכה רק לאחסן ולקרוא את נתוני האפליקציה.

כדי להשיג את זה, מודול התכונה תלוי במודול ההפשטה ולא בהטמעה ספציפית של מסד נתונים. ההפשטה הזו מגדירה את ה-API של מסד הנתונים של האפליקציה. במילים אחרות, הוא מגדיר את הכללים לאינטראקציה עם מסד הנתונים. כך מודול התכונות יכול להשתמש בכל מסד נתונים בלי לדעת את פרטי ההטמעה הבסיסיים שלו.

מודול ההטמעה הקונקרטי מספק את ההטמעה בפועל של ממשקי ה-API שמוגדרים במודול האבסטרקטי. כדי לעשות את זה, מודול ההטמעה תלוי גם במודול האבסטרקציה.

הזרקת תלות

בשלב הזה אתם בטח תוהים איך מודול התכונות קשור למודול ההטמעה. התשובה היא Dependency Injection. מודול התכונות לא יוצר ישירות את מופע מסד הנתונים הנדרש. במקום זאת, הוא מציין את התלות שלו. לאחר מכן, התלויות האלה מסופקות באופן חיצוני, בדרך כלל במודול האפליקציה.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

יתרונות

היתרונות של הפרדת ממשקי ה-API וההטמעות שלהם הם:

  • אפשרות החלפה: הפרדה ברורה בין מודולים של API ומודולים של הטמעה מאפשרת לפתח כמה הטמעות לאותו API ולעבור ביניהן בלי לשנות את הקוד שמשתמש ב-API. האפשרות הזו יכולה להיות שימושית במיוחד בתרחישים שבהם רוצים לספק יכולות או התנהגויות שונות בהקשרים שונים. לדוגמה, הטמעה מדמה לבדיקה לעומת הטמעה אמיתית לייצור.
  • הפרדה: ההפרדה הזו אומרת שמודולים שמשתמשים בהפשטות לא תלויים באף טכנולוגיה ספציפית. אם תבחרו לשנות את מסד הנתונים מ-Room ל-Firestore בשלב מאוחר יותר, יהיה לכם קל יותר לעשות את זה כי השינויים יתבצעו רק במודול הספציפי שמבצע את העבודה (מודול ההטמעה) ולא ישפיעו על מודולים אחרים שמשתמשים ב-API של מסד הנתונים.
  • יכולת בדיקה: הפרדה בין ממשקי API לבין ההטמעות שלהם יכולה להקל מאוד על הבדיקה. אפשר לכתוב מקרי בדיקה בהתאם לחוזים של ה-API. אפשר גם להשתמש בהטמעות שונות כדי לבדוק תרחישים שונים ומקרים קיצוניים, כולל הטמעות מדומות.
  • שיפור בביצועי ה-build: כשמפרידים בין API לבין ההטמעה שלו למודולים שונים, שינויים במודול ההטמעה לא מחייבים את מערכת ה-build לבצע קומפילציה מחדש של המודולים שתלויים במודול ה-API. כך מקצרים את זמני הפיתוח ומגדילים את הפרודוקטיביות, במיוחד בפרויקטים גדולים שבהם זמני הפיתוח יכולים להיות משמעותיים.

מתי צריך להפריד

כדאי להפריד בין ממשקי ה-API לבין ההטמעות שלהם במקרים הבאים:

  • יכולות מגוונות: אם אפשר להטמיע חלקים מהמערכת בכמה דרכים, ממשק API ברור מאפשר להחליף בין הטמעות שונות. לדוגמה, יכול להיות שיש לכם מערכת רינדור שמשתמשת ב-OpenGL או ב-Vulkan, או מערכת חיוב שפועלת עם Play או עם ה-API הפנימי שלכם לחיוב.
  • כמה אפליקציות: אם מפתחים כמה אפליקציות עם יכולות משותפות לפלטפורמות שונות, אפשר להגדיר ממשקי API משותפים ולפתח הטמעות ספציפיות לכל פלטפורמה.
  • צוותים עצמאיים: ההפרדה מאפשרת למפתחים או לצוותים שונים לעבוד על חלקים שונים של בסיס הקוד בו-זמנית. מפתחים צריכים להתמקד בהבנת חוזי ה-API ובשימוש נכון בהם. הם לא צריכים לדאוג לגבי פרטי ההטמעה של מודולים אחרים.
  • בסיס קוד גדול: כשבסיס הקוד גדול או מורכב, הפרדה של ה-API מההטמעה הופכת את הקוד לקל יותר לניהול. הוא מאפשר לכם לפצל את בסיס הקוד ליחידות מפורטות יותר, מובנות יותר וקלות יותר לתחזוקה.

איך מטמיעים?

כדי להטמיע היפוך תלות, צריך לפעול לפי השלבים הבאים:

  1. יצירת מודול הפשטה: המודול הזה צריך להכיל ממשקי API (ממשקים ומודלים) שמגדירים את ההתנהגות של התכונה.
  2. יצירת מודולים להטמעה: מודולים להטמעה צריכים להסתמך על מודול ה-API ולהטמיע את ההתנהגות של הפשטה.
    במקום שמודולים ברמה גבוהה יהיו תלויים ישירות במודולים ברמה נמוכה, מודולים ברמה גבוהה ומודולים להטמעה תלויים במודול ההפשטה.
    איור 10. מודולי ההטמעה תלויים במודול ההפשטה.
  3. הגדרת מודולים ברמה גבוהה כתלויים במודולים של הפשטה: במקום להגדיר את המודולים כתלויים ישירות בהטמעה ספציפית, מגדירים אותם כתלויים במודולים של הפשטה. מודולים ברמה גבוהה לא צריכים לדעת פרטים על ההטמעה, אלא רק את החוזה (API).
    מודולים ברמה גבוהה מסתמכים על הפשטות, ולא על יישום.
    איור 11. מודולים ברמה גבוהה תלויים בהפשטות, ולא בהטמעה.
  4. הוספת מודול הטמעה: לבסוף, צריך להוסיף את ההטמעה בפועל של התלויות. ההטמעה הספציפית תלויה בהגדרת הפרויקט, אבל בדרך כלל מודול האפליקציה הוא מקום טוב לעשות את זה. כדי לספק את ההטמעה, צריך לציין אותה כתלות עבור וריאנט הבנייה או קבוצת מקורות הבדיקה שנבחרו.
    מודול האפליקציה מספק הטמעה בפועל.
    איור 12. מודול האפליקציה מספק הטמעה בפועל.

שיטות מומלצות כלליות

כמו שציינו בהתחלה, אין דרך נכונה אחת לפתח אפליקציה מרובת מודולים. בדיוק כמו שיש הרבה ארכיטקטורות תוכנה, יש גם הרבה דרכים להפוך אפליקציה למודולרית. עם זאת, ההמלצות הכלליות הבאות יכולות לעזור לכם להפוך את הקוד לקריא יותר, לניתן לתחזוקה ולניתן לבדיקה.

שמירה על עקביות בהגדרות

כל מודול מוסיף תקורה להגדרה. אם מספר המודולים מגיע לסף מסוים, קשה לשמור על הגדרה עקבית. לדוגמה, חשוב שהמודולים ישתמשו בתלות מאותה גרסה. אם צריך לעדכן מספר גדול של מודולים רק כדי לשדרג את גרסת התלות, זה לא רק מאמץ אלא גם פתח לטעויות פוטנציאליות. כדי לפתור את הבעיה הזו, אפשר להשתמש באחד מהכלים של gradle כדי לרכז את ההגדרות:

  • קטלוגים של גרסאות הם רשימה של תלות שנוצרת על ידי Gradle במהלך הסנכרון. זהו מקום מרכזי להצהרה על כל התלויות, והוא זמין לכל המודולים בפרויקט.
  • משתמשים בתוספי מוסכמות כדי לשתף לוגיקה של בנייה בין מודולים.

חשיפה מינימלית

הממשק הציבורי של מודול צריך להיות מינימלי ולחשוף רק את הרכיבים החיוניים. היא לא אמורה לחשוף פרטים על ההטמעה שלה. היקף הכל צריך להיות קטן ככל האפשר. משתמשים בהיקף החשיפה private או internal של Kotlin כדי להגדיר את ההצהרות כפרטיות למודול. כשמצהירים על יחסי תלות במודול, עדיף להשתמש ב-implementation במקום ב-api. האחרון חושף תלויות טרנזיטיביות לצרכנים של המודול. שימוש ב-implementation עשוי לשפר את זמן הבנייה, כי הוא מקטין את מספר המודולים שצריך לבנות מחדש.

העדפה של מודולים ב-Kotlin וב-Java

יש שלושה סוגים חיוניים של מודולים שנתמכים ב-Android Studio:

  • מודולים של אפליקציות הם נקודת כניסה לאפליקציה. הם יכולים להכיל קוד מקור, משאבים, נכסים וקובץ AndroidManifest.xml. הפלט של מודול אפליקציה הוא קובץ Android App Bundle‏ (AAB) או חבילת אפליקציה ל-Android‏ (APK).
  • מודולים של ספריות מכילים את אותו תוכן כמו מודולים של אפליקציות. הם משמשים מודולים אחרים של Android כתלות. הפלט של מודול ספרייה הוא קובץ Android Archive ‏ (AAR). קובץ כזה זהה מבחינה מבנית למודולים של אפליקציות, אבל הוא עובר קומפילציה לקובץ Android Archive ‏ (AAR) שאפשר להשתמש בו מאוחר יותר במודולים אחרים כתלות. מודול ספרייה מאפשר להשתמש באותה לוגיקה ובאותם משאבים בכמה מודולים של אפליקציות.
  • ספריות Kotlin ו-Java לא מכילות משאבים, נכסים או קובצי מניפסט של Android.

מודולים של Android מגיעים עם תקורה, ולכן מומלץ להשתמש במודולים מסוג Kotlin או Java כמה שיותר.