מצא את כל הקוראים של פונקציה מיושנת

כיצד למצוא את כל הקוראים של פונקציה מיושנת לפני הסרתה

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

מצא כל מתקשר לפני שאתה מסיר כל דבר

SMART TS XL בונה גרף שיחות חוצה שפות שמזהה כל מתקשר מכל פונקציה לפני ביצוע שינוי.

לחץ כאן

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

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

למה גילוי מתקשרים קשה יותר ממה שזה נראה

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

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

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

קריאות ישירות מעבר לגבולות מאגר

קריאות ישירות הן סוג הקריאה הפשוט ביותר: פונקציה אחת קוראת במפורש לאחרת בשם. בתוך מאגר יחיד, IDEs מטפלים בהן בצורה אמינה. מעבר לגבולות המאגרים, הניתוח נכשל מכיוון שהאינדוקס של ה-IDE אינו חוצה את הגבול. אם מאגר A מגדיר פונקציית שירות משותפת ומאגרים B, C ו-D כל אחד מייבא וקורא לה, ה-IDE עבור כל אחד מהמאגרים הללו רואה רק את הקריאות הנמצאות בטווח המאונדקס שלו.

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

שיחות עקיפות: עוטפות, נציגים וחזיתות

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

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

נבחן דוגמה מייצגת של Java שבה נגישה שיטה שהוצאה משימוש דרך שכבת האצלה:

תאווה

// Deprecated in the core service
@Deprecated
public BillingResult calculateLegacyFee(Account account) {
    // original implementation
}

// Facade that delegates; not visible as a direct caller in a simple reference search
public BillingResult computeFee(Account account) {
    return calculateLegacyFee(account);  // indirect caller
}

// Actual consumer; calls computeFee, unaware of the underlying deprecated method
public void processMonthlyBilling(List<Account> accounts) {
    accounts.forEach(a -> computeFee(a));  // two hops from the deprecated function
}

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

קריאות בין-לשוניות

קריאות בין-לשוניות הן הקטגוריה שבה כלי גילוי קוראים סטנדרטיים נכשלים בצורה המוחלטת ביותר. כאשר שירות Java קורא לתוכנית COBOL לפי שם דרך שכבת תוכנה, כאשר סקריפט Python קורא לפרוצדורה מאוחסנת שעוטפת פונקציה מיושנת, או כאשר משימת JCL קוראת לתוכנית לפי PROGNAME שלה שקוראת באופן פנימי לפסקה מיושנת, אף אחת מהקשרים הללו לא מופיעה בגרף הקריאה של אף שפה. כלי העבודה של כל שפה רואה רק את הצד שלו של הקריאה.

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

שיגור דינמי והשתקפות

חלק מהקוראים קוראים לפונקציה לא לפי שמה המילולי בקוד המקור אלא באמצעות מנגנון שפותר את הפונקציה בזמן ריצה: השתקפות ב-Java או .NET, getattr/__call__ בפייתון, קשירה מאוחרת ב-COBOL דרך CALL identifier, שיגור דינמי באמצעות פולימורפיזם, או קריאה לפי מחרוזת במסגרות תוספים ובמערכות מונחות תצורה. קוראים אלה אינם מכילים את שם הפונקציה שהוצאה משימוש בשום צורה שניתוח סטטי יכול לזהות באופן מהימן.

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

הכלים שאליהם מפתחים פונים תחילה והיכן הם עוצרים

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

היררכיית קריאות IDE: אמינות בתוך פרויקט אחד

תכונות היררכיית קריאות IDE הן הצעד הראשון הטבעי ביותר. IntelliJ IDEA, Visual Studio, VS Code ו-Eclipse מספקים צורה כלשהי של "מצא את כל הקוראים" או "הצג היררכיית קריאות" אשר מונה באופן רקורסיבי את הקוראים של פונקציה נבחרת, בתוך ההיקף המאונדקס של הפרויקט או סביבת העבודה הנוכחית. עבור פונקציה המשמשת אך ורק בתוך מאגר אחד ושפה אחת, תכונות אלו מדויקות ומספקות.

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

grep וחיפוש טקסט: רחב אך עיוור מבחינה מבנית

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

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

אזהרות מהדר ו @Deprecated ביאורים

שפות מודרניות ושרשראות כלים מספקות מנגנוני ביאור של פונקציות שהוצאו משימוש שמייצרים אזהרות כאשר פונקציות שהוצאו משימוש נקראות. של ג'אווה @Deprecated ביאור משולב עם -Xlint:deprecation מייצר אזהרות בזמן קומפילציה באתרי קריאה. C# [Obsolete] התכונה מייצרת אזהרות בזמן הבנייה. המוסכמה של Go למתן שמות לפונקציות שהוצאו משימוש ולתעד אותן ב-godoc אינה מייצרת אזהרות באופן אוטומטי. מנגנונים אלה בעלי ערך אך מוגבלים באופן ספציפי: הם פועלים רק עבור קוראים שמקומפים כנגד אותו בסיס קוד שבו צוין התכונה שהוצאה משימוש.

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

כלי ניתוח סטטי: טובים יותר, אך מוגבלים בהיקף

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

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

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

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

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

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

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

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

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

תהליך הוצאה משימוש עד להסרה: גישה מובנית

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

שלב 1: סמן ומדידה

השלב הראשון הוא סימון הפונקציה כמיושבת באמצעות המנגנון המובנה של השפה (@Deprecated בג'אווה, [Obsolete] ב-C#, #[deprecated] ב-Rust, או במקבילה המתאימה) וקביעת ספירת קוראים בסיסית. בסיס זה אינו תוצאה של חיפוש בודד; הוא תוצאה של אינדוקס של כל בסיס קוד ידוע שעשוי לקרוא לפונקציה וספירת התוצאות. בסיס הבסיס משרת שתי מטרות: הוא מכמת את היקף ההעברה, והוא מספק התייחסות שמולה ניתן למדוד את ההתקדמות ככל שהקוראים מועברים.

יש לארגן את קו הבסיס לפי סוג המתקשר ומיקום:

קטגוריית המתקשרלִסְפּוֹרעדיפותבעלים
מתקשרים ישירים באותו מאגרNגָבוֹהַהקבוצה הנוכחית
מתקשרים ישירים בשירותים תלוייםNגָבוֹהַבעלי השירות
מתקשרים דרך פונקציות עטיפהNבינוניבעלי עטיפה
קוראים בקוד שנוצר או בקוד במסגרתNבינוניצוות המסגרת
מתקשרים בקוד לבדיקה בלבדNנמוךהקבוצה הנוכחית
מתקשרים בקוד מתNניקוי בלבדהקבוצה הנוכחית

שלב 2: הודעה והעברה

עם רשימת מלאי מתקשרים, ההעברה הופכת למאמץ מאורגן ולא ריאקטיבי. כל בעל מתקשר מקבל הודעה על מיקומי השיחה הספציפיים: לא "ייתכן שאתה מתקשר לפונקציה הזו" אלא "אתה מתקשר לפונקציה הזו בשורה 247 של BillingService.java, שורה 82 של AccountProcessor.java, ובמבחן האינטגרציה בשורה 14 של BillingServiceTest.javaרמת ספציפיות זו היא מה שמאפשר מלאי הקוראים ואזהרות גנריות על הוצאה משימוש אינן יכולות לספק.

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

שלב 3: ניטור ספירת המתקשרים

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

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

שלב 4: אימות שלמות לפני ההסרה

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

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

כיצד גילוי מתקשרים פועל בצורה שונה בסביבות מדור קודם ומיינפריים

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

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

בסביבות JCL, תוכניות מופעלות לפי שם במשפטי EXEC PGM=. שם התוכנית הוא מחרוזת הממופה למודול שעבר קומפילציה בספריית טעינה. מעקב אחר קוראים של תוכנית COBOL דרך JCL דורש ניתוח של ה-JCL כדי לחלץ שמות תוכניות, מיפוי שמות אלה לתוכניות COBOL שעברו קומפילציה שמיישמות אותם, וזיהוי אילו פסקאות COBOL בתוך תוכניות אלה קוראות לכלי השירות שהוצא משימוש. פתרון רב-שלבי זה חורג לחלוטין מתחום הפעולה של מנתח COBOL או מנתח JCL הפועל בנפרד.

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

איך SMART TS XL בונה את מלאי המתקשרים המלא

SMART TS XL בונה גרף קריאה מאוחד בכל שפה, פלטפורמה ומאגר בסביבה המאונדקסת. תוכניות COBOL, זרמי משימות JCL, שירותי Java, יישומי .NET, פרוצדורות מאוחסנות של SQL, סקריפטים של Python וארטיפקטים אחרים של מקור, כולם מנותחים באמצעות ניתוח ספציפי לשפה לתוך גרף הפניה צולבת משותף. כל פונקציה, פסקה, פרוצדורה, שיטה ויחידת תוכנית הם צומת בגרף זה. כל קשר קריאה, בין אם משפט COBOL CALL, קריאה לשיטת Java, JCL EXEC PGM או SQL EXEC, הוא קצה טיפוסי. הגרף מייצג את טופולוגיית הקריאה המלאה של המערכת, ולא תצוגה חלקית לכל שפה.

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

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

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

הסרת פונקציה ללא חרטה

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

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

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