פרק 101 - מונוריפו, מולטי-ריפו ומה שביניהם
מונוריפו מול מולטי-ריפו הוא לא ויכוח על Git. גיל תייר מראה איך פקג׳ים, בדיקות וגבולות תלות מונעים מקודבייס להפוך לספגטי, גם כשהצוות והקוד גדלים.
צפו בהרצאההסיפור שמאחורי ההרצאה
- מונוריפו ומולטי-ריפו הם פרטי מימוש. הבעיה האמיתית היא תלות בלתי נשלטת בין חלקי הקוד.
- פקג׳ הוא יחידת ארכיטקטורה עצמאית: בונים, בודקים ומפרסמים אותו בנפרד, וצורכים אותו דרך גרסה מוגדרת.
- מונוריפו קלאסי עם build ו-test לכל הקוד בכל שינוי משמר את הכאב, גם כשהפקג׳ים נמצאים באותו repository.
- הגישה שגיל מתאר מקריבה שינוי מקביל בשני פקג׳ים כדי לאפשר בדיקה ופרסום של פקג׳ אחד בכל פעם.
- המחיר הזה עובד רק כשכל פקג׳ מכוסה בבדיקות שאפשר לסמוך עליהן.
מונוריפו מול מולטי-ריפו נשמע כמו ויכוח על Git. בפועל, Git הוא רק הארון שבו הקוד יושב. השאלה החשובה היא מי תלוי במי, מי יכול להשתנות בלי לשבור אחרים, וכמה זמן לוקח לצוות להבין מה באמת קרה כשמשהו השתנה.
בלייב במיקרוסופט, גיל תייר מפרק את הוויכוח מהזווית הנכונה: לא “איזה repository לבחור”, אלא איך בונים קודבייס שעדיין אפשר להבין, לבדוק ולפרסם גם אחרי שהצוות והמערכת גדלו. הטענה שלו חדה: אפשר לשים הכול באותו repository ועדיין להישאר עם מונולית כואב. אפשר גם להחזיק הרבה פקג׳ים בלי להפוך את הארגון למפעל של clone, pull וגרסאות שנשכחו.
קודם כל, להבדיל בין שני סוגי מונוריפו
גיל משתמש בשתי הגדרות. פרויקט מונוריפו הוא repository יחיד שיש בו כמה אפליקציות, מודולים או פקג׳ים שיכולים להיבנות פחות או יותר באופן עצמאי. Babel, לדוגמה, עבר ממצב שבו פקג׳ים רבים ישבו בריפוזיטוריז נפרדים למצב שבו כולם נמצאים יחד.
מעליו יש קמפני מונוריפו: repository אחד שמכיל לא רק כמה פקג׳ים של מוצר אחד, אלא הרבה פרויקטים של הארגון כולו. גיל מביא את Google כדוגמה. מולטי-ריפו, לעומת זאת, הוא אוסף ריפוזיטוריז נפרדים שמנוהלים כחלק מאותו פרויקט או ארגון.
ההבחנה חשובה כי “מונוריפו” אינו ארכיטקטורה. זה מיקום. גם מונוריפו יכול להיות אוסף של חלקים שמנוהלים כמונולית אחד. וגם מולטי-ריפו יכול להחזיק פקג׳ים עם גבולות נקיים. ההחלטה על מספר הריפוזיטוריז לא מחליפה החלטה על מבנה הקוד.
מונו ריפו זה גיט ריפו אחד.
הבעיה מתחילה הרבה לפני ה-build
חברה צעירה מתחילה פשוט. frontend בריפו אחד, backend בריפו אחר, כמה מפתחים, כמה פולדרים. ואז יש עוד feature ועוד util ועוד קיצור דרך קטן. קובץ באזור אחד מייבא קובץ מאזור אחר, ואחר כך גם להפך. ה-dependency graph מפסיק להיות עץ ומתחיל להיראות כמו קערת ספגטי.
זה לא רק עניין של סדר בתיקיות. כשמודול נמוך יכול לקרוא למשהו גבוה, וכשכולם יכולים לייבא מכל מקום, ה-bounded contexts נעלמים. מפתח חדש לא יודע איפה מתחילה אחריות אחת ואיפה נגמרת אחרת. circular dependencies הופכים מ”נסדר אחר כך” לסיבה שבגללה קשה לבדוק, לשנות ולהבין את המערכת.
הפיתוי הרגיל הוא להוציא shared code לריפו אחר: utilities, design system, קוד של צוות תשתיות. בהתחלה זה מרגיש כמו סדר. אחרי כמה הוצאות כאלה מתקבל מולטי-ריפו, וכמות הפעולות סביב כל שינוי מתחילה לגדול. כל פקג׳ הוא ריפו, כל שינוי דורש לעבור בין ריפוזיטוריז, וכל dependency מרגיש כמו תהליך תפעולי.
פקג׳ים הם הגבול, לא תיקיות
הפתרון שגיל מציע אינו להאמין יותר ב-reuse. להפך. הוא דורש להיות הרבה יותר קשוחים לגבי האופן שבו עושים reuse. מחלקים את הקודבייס לפקג׳ים עצמאיים, וכל פקג׳ צורך פקג׳ אחר דרך dependency מפורשת - למשל ב-package.json - ולא דרך import פנימי שמדלג בין תיקיות.
כך התלות הופכת לגלויה. לפקג׳ יש גרסה, בדיקות, build וגבול. אפשר להבין אותו כיחידה בפני עצמה. זו החלפה מכוונת של בעיית קוד בבעיית ארכיטקטורה: פחות חשוב כרגע איך כתובה פונקציה אחת, ויותר חשוב איזה פקג׳ רשאי להכיר איזה פקג׳.
המחיר הוא שלא כל קוד דומה חייב להפוך ל-shared utility. בהרצאה עולה גם המקרה שבו אותה פונקציה קיימת בשני contexts. לפעמים כפילות קטנה עדיפה על dependency שמחברת שני תחומים שלא אמורים להיות מחוברים. reuse הוא לא מטרה. הפרדה היא המטרה.
למה מונוריפו קלאסי שומר את הכאב
כאן מגיעה הביקורת של גיל על מה שהוא קורא לו מונוריפו קלאסי. יש הרבה פקג׳ים באותו repository, אבל הכלים עדיין רואים אותם כמונולית: build אחד גדול, סט בדיקות גדול, תהליך פרסום אחד גדול. שינוי בכפתור ב-UI עלול לגרור build ופריסה של עשרות services.
אפשר לזרוק עוד workers ולקצר זמן. אבל זה מטפל במחיר, לא במבנה. הבעיה העמוקה יותר היא שהקוד והיחסים בין החלקים ממשיכים לגדול יחד. כשהכול נבדק יחד, קל מאוד להמשיך לשנות כמה פקג׳ים באותו PR ולשמור בפועל על coupling שלא רואים מיד.
כלים כמו NX ו-Turborepo, לפי גיל, מצוינים למונוריפו הקלאסי. הם אינם המטרה של ההרצאה. השאלה היא האם הצוות רוצה לייעל build גדול - או להפסיק להניח שצריך build גדול לכל שינוי.
הוויתור הכואב: פקג׳ אחד בכל שינוי
הגישה של גיל דורשת ויתור שמפתחי מוצר לא אוהבים: לא מפתחים שני פקג׳ים במקביל. אם צריך לשנות קומפוננטה ואת האפליקציה שצורכת אותה, מתחילים בפקג׳ הנמוך. כותבים, בודקים, מפרסמים אותו ל-private registry, מעדכנים את הגרסה באפליקציה ורק אז ממשיכים אליה.
מבטרים על היכולת לפתח שני פקג׳ים בו זמנית.
זה נשמע איטי כי ה-local development כבר לא מחבר הכול באמצעות npm link או symlink. אבל גיל מתאר למה זה הופך פשוט יותר: PR אחד נוגע בפקג׳ אחד, ה-CI יודע מה לבנות ומה לבדוק, והפרסום הוא artefact מוגדר שאפליקציה אחרת בוחרת לצרוך. אין צורך להריץ את כל הקודבייס רק כדי לבדוק שינוי מקומי.
המפתח הוא בדיקות. בלי בדיקות בפקג׳ הנמוך, אין דרך לדעת שהוא תקין לפני שמשלבים אותו באפליקציה, וכל השיטה נשברת. npm link נוח בדיוק משום שהוא עוקף את החיכוך הזה. לכן הוא גם עלול לאפשר לצוות לוותר על הבדיקה והפרסום העצמאיים. ב-Round Forest, גיל מספר, השימוש בו נעלם עם הזמן כשהצוות התרגל לעבוד, לבדוק ולפרסם פקג׳ אחד בכל פעם.
לא חייבים לבנות את כל העץ
ההתנגדות המתבקשת היא גרסאות. אם פקג׳ בסיסי התעדכן, האם לא צריך לבנות ולבדוק כל פקג׳ שתלוי בו, וכל מי שתלוי בהם? גיל עונה שלא. אם השינוי נעשה בשביל אפליקציה מסוימת, מעדכנים את ה-dependency של אותה אפליקציה ובונים אותה. חלק אחר בעץ יפגוש את הגרסה החדשה רק כשיבחר לעדכן אותה.
זו לא הבטחה שלא יהיו באגים. זו החלטה לא לשלם מראש על בעיה שלא קיימת. כמו בכל package חיצוני, באג יתגלה כשהוא משפיע על הצרכן שבאמת אימץ את הגרסה. בתמורה, הצוות מקבל תהליך שבו כל שינוי קטן לא מפעיל את כל המערכת.
השורה התחתונה
מונוריפו מול מולטי-ריפו הוא ויכוח שימושי רק אחרי שמחליטים מהו גבול של פקג׳, איך תלות נעשית גלויה, ואיפה עוברת האחריות על בדיקה ופרסום. ההרצאה של גיל לא נותנת בחירת ברירת מחדל לכל חברה. היא דורשת להפסיק להשתמש ב-repository כתשובה ארכיטקטונית.
- בחרו פקג׳ים לפי אחריות וגבולות תלות, לא לפי מספר תיקיות.
- אל תתנו ל-import פנימי לעקוף את הגבולות שהגדרתם.
- אל תסמכו על build ענק כדי להוכיח ששינוי קטן בטוח.
- אם מאמצים שינוי של פקג׳ אחד בכל פעם, משקיעים קודם בבדיקות שלו.
זה פחות נוח ברגע הראשון. אבל זה נוח מהסוג שמחזיק כשהקודבייס כבר לא קטן, כשהצוות כבר לא יושב סביב אותו שולחן, וכש”נסדר אחר כך” הפך לחוב שאי אפשר יותר להתעלם ממנו.
"מונו ריפו זה גיט ריפו אחד."
- גיל תייר
"כשאנחנו מגיעים ל-400 פקג׳ים, אז הרעיון של המולטי ריפו כבר לא עובד."
- גיל תייר
"מבטרים על היכולת לפתח שני פקג׳ים בו זמנית."
- גיל תייר
- 02:19 ההגדרות: פרויקט מונוריפו, קמפני מונוריפו ומולטי-ריפוגיל מפריד בין repository יחיד לפרויקט עם כמה פקג׳ים לבין repository ארגוני שמכיל פרויקטים רבים, ומגדיר מולטי-ריפו דרך הדוגמה של Babel.
- 07:28 איך קוד גדל והופך לספגטיהמעבר מפולדרים, דרך ריפוזיטוריז נפרדים, לפקג׳ים מראה למה circular dependencies והגדרות גבול חלשות הופכים קודבייס לבלתי ניתן לתחזוקה.
- 14:15 למה build אחד גדול אינו הפתרוןבמונוריפו קלאסי הכלים מתייחסים לאוסף אפליקציות ופקג׳ים כמונולית אחד, ולכן זמני build ו-test גדלים עם הקודבייס.
- 23:33 פקג׳ים, bounded contexts ותלויות גלויותגיל מסביר איך צריכת תלות דרך package manager במקום import פנימי נותנת לכל פקג׳ גבול ברור ומאפשרת להבין אותו בפני עצמו.
- 28:48 הוויתור שמאפשר סקיילבמקום לשנות שני פקג׳ים יחד באותו PR, מתחילים בפקג׳ הנמוך, בודקים ומפרסמים אותו, ואז מעדכנים את הפקג׳ הצורך.
- 31:54 למה npm link מחליש את המשמעתחיבור מקומי בין פקג׳ים נוח לפיתוח, אבל עלול לאפשר לעקוף את הבדיקות והפרסום הנפרד שהגישה נשענת עליהם.
- 35:34 NX, Turborepo והסיבה ל-build הגדולהכלים מתאימים למונוריפו קלאסי, אך גיל מסביר למה שינוי במספר פקג׳ים מחזיר את הצורך למיין ולבנות עץ תלות רחב.
- 38:02 מעדכנים רק את מה שהאפליקציה צריכהלאחר פרסום פקג׳, אין הכרח לבנות את כל העץ שמעליו: מעדכנים ובונים את האפליקציה שבאמת צריכה את הגרסה החדשה.
מה כוסה בהרצאה
שאלות מההרצאה
מה ההבדל בין מונוריפו למולטי-ריפו?
לפי ההגדרה שגיל משתמש בה, מונוריפו הוא Git repository אחד שמכיל יותר מאפליקציה, מודול או פקג׳ אחד. מולטי-ריפו מחלק פקג׳ים לריפוזיטוריז נפרדים. הוא גם מבחין בין פרויקט מונוריפו לבין קמפני מונוריפו - repository ארגוני שמכיל פרויקטים רבים.
למה מונוריפו קלאסי נהיה איטי?
כשה-build, הבדיקות והפרסום מתייחסים לכל הפקג׳ים כיחידה אחת, כל שינוי גורר עבודה רחבה יותר. ככל שהקודבייס גדל, גדלים גם זמני build ו-test, והצוות מתחיל להשקיע באופטימיזציות במקום בגבולות הארכיטקטוניים עצמם.
מה התנאי לפיתוח פקג׳ אחד בכל פעם?
לכל פקג׳ חייבות להיות בדיקות שאפשר לסמוך עליהן. אחרת אי אפשר לפרסם אותו בביטחון לפני שבודקים אותו דרך האפליקציה שצורכת אותו, וההפרדה בין הפקג׳ים מתפרקת.
האם צריך לבנות מחדש את כל התלויות אחרי עדכון פקג׳?
לא בהכרח. גיל מציע לעדכן את הגרסה החדשה במקום שבו היא נחוצה בפועל ולבנות את האפליקציה הצורכת אותה. בעיה בחלק אחר של העץ תתגלה כשאותו חלק יעדכן את התלות שלו.