החלפת זמנית בשאילתה: טכניקת שיפוץ

החלפת קוד זמני בשאילתה: טכניקת שיפוץ קוד (refactoring) לקוד נקי וניתן לבדיקה יותר

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

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

בנו מחדש את הקוד שלכם בביטחון

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

למד עוד

מהו משתנה זמני (Temp) בתכנות?

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

פִּיתוֹן

# Python: base_price is a temp variable
def calculate_total(quantity, item_price):
    base_price = quantity * item_price   # temp: computed once, used below
    if base_price > 1000:
        return base_price * 0.95
    return base_price * 0.98

תאווה

// Java: basePrice is a temp variable
double basePrice = quantity * itemPrice;   // temp
if (basePrice > 1000) {
    return basePrice * 0.95;
}
return basePrice * 0.98;

כתב כתיבה

// TypeScript: basePrice is a temp variable
const basePrice = quantity * itemPrice;    // temp
if (basePrice > 1000) return basePrice * 0.95;
return basePrice * 0.98;

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

מהו רפקטורינג בהנדסת תוכנה?

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

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

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

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

מה זה להחליף זמני עם שאילתה?

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

הדוגמה הקנונית מ- Refactoring של פאולר :

לפני:

תאווה

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
    return basePrice * 0.95;
else
    return basePrice * 0.98;

אחרי:

תאווה

if (basePrice() > 1000)
    return basePrice() * 0.95;
else
    return basePrice() * 0.98;

private double basePrice() {
    return _quantity * _itemPrice;
}

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

הבעיה עם משתנים זמניים

הם מפזרים לוגיקה על פני שיטה

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

הם חוסמים את שיטת החילוץ

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

הם מזמינים שימוש חוזר ומוטציה

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

שלב אחר שלב: כיצד להחיל את האפשרות "החלף זמני" באמצעות שאילתה

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

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

שלב 2: חילוץ הצד הימני של המטלה לתוך מתודה פרטית. תן לשיטה שם שמתאר מה היא מחשבת, לא איך. basePrice() טוב יותר מ calculateQuantityTimesPrice().

שלב 3: החלף כל הפניה לביטוי temp בקריאה למתודה החדשה. רוב מערכות ה-IDE יכולות לעשות זאת באופן אוטומטי: לחץ לחיצה ימנית על temp → Refactor → Inline Variable, לאחר מכן Extract Method על הביטוי המוטבע.

שלב 4: מחיקת הצהרת משתנה ה-temp. אם החילוץ הושלם, ל-temp אמורות להיות הפניות וניתן להסירו.

ג'אווה: דוגמה מלאה לעבודה

תאווה

// Before: Order class with temporary variables
public class Order {
    private int quantity;
    private double itemPrice;

    public double getPrice() {
        double basePrice    = quantity * itemPrice;        // temp 1
        double discountFactor;                             // temp 2
        if (basePrice > 1000)
            discountFactor = 0.95;
        else
            discountFactor = 0.98;
        return basePrice * discountFactor;
    }
}

תאווה

// After: temps extracted to query methods
public class Order {
    private int quantity;
    private double itemPrice;

    public double getPrice() {
        return basePrice() * discountFactor();
    }

    private double basePrice() {
        return quantity * itemPrice;
    }

    private double discountFactor() {
        return basePrice() > 1000 ? 0.95 : 0.98;
    }
}

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

פייתון: החלפת זמנית במאפיין

בפייתון, המקבילה הטבעית של מתודת שאילתה היא א @property, מה שמאפשר קריאה למתודה ללא סוגריים וקריאה זהה למאפיין access:

פִּיתוֹן

# Before: temporary variables in a method
class Order:
    def __init__(self, quantity, item_price):
        self.quantity   = quantity
        self.item_price = item_price

    def get_price(self):
        base_price     = self.quantity * self.item_price  # temp
        discount       = 0.95 if base_price > 1000 else 0.98  # temp
        return base_price * discount

פִּיתוֹן

# After: temps replaced with properties (query methods in Python)
class Order:
    def __init__(self, quantity, item_price):
        self.quantity   = quantity
        self.item_price = item_price

    def get_price(self):
        return self.base_price * self.discount_factor

    @property
    def base_price(self):
        return self.quantity * self.item_price

    @property
    def discount_factor(self):
        return 0.95 if self.base_price > 1000 else 0.98

שימוש @property אומר self.base_price קורא באופן זהה למשתנה מופע, מה שהופך את קוד הקריאה self.base_price * self.discount_factor טבעי לחלוטין. כל תכונה ניתנת לבדיקה באופן עצמאי:

פִּיתוֹן

def test_base_price():
    order = Order(10, 150)
    assert order.base_price == 1500

def test_discount_factor_high_value():
    order = Order(10, 150)   # base_price = 1500 > 1000
    assert order.discount_factor == 0.95

def test_get_price():
    order = Order(10, 150)
    assert order.get_price() == 1500 * 0.95

רמת בדיקה זו בלתי אפשרית עם הגרסה מבוססת הזמנית: החישובים הפנימיים base_price ו discount_factor אינם נגישים מחוץ לשיטה.

TypeScript: שיטות שאילתה ו-Getters

TypeScript תומך הן בשאילתות מבוססות מתודה והן ב-property getters, ותואם את התבניות הזמינות ב-Java וב-Python בהתאמה:

כתב כתיבה

// Before: temporary variables
class Order {
    constructor(private quantity: number, private itemPrice: number) {}

    getPrice(): number {
        const basePrice = this.quantity * this.itemPrice;  // temp
        const discount  = basePrice > 1000 ? 0.95 : 0.98; // temp
        return basePrice * discount;
    }
}

כתב כתיבה

// After: TypeScript getters replace temps
class Order {
    constructor(private quantity: number, private itemPrice: number) {}

    getPrice(): number {
        return this.basePrice * this.discountFactor;
    }

    private get basePrice(): number {
        return this.quantity * this.itemPrice;
    }

    private get discountFactor(): number {
        return this.basePrice > 1000 ? 0.95 : 0.98;
    }
}

מתן שמות לשיטות שאילתה בצורה נכונה

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

שמות טובים של שיטות שאילתה פועלים לפי העקרונות הבאים:

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

השתמשו בביטויי עצם עבור ערכים. שיטות שאילתה מחזירות ערכים; הן אינן פקודות. discountFactor(), totalAmount(), isEligible() לפעול לפי המוסכמה של מתן שמות למה שמוחזר. calculateDiscount(), processAmount() לפעול לפי המוסכמה הציווית של פקודות, דבר המבלבל עבור שיטות שמחשבת ומחזירה בלבד.

שאילתות בוליאניות צריכות להיקרא כשאלות. isHighValue(), hasDiscount(), meetsThreshold() להעביר את המסר שהתשובה היא ערך בוליאני ושהמתקשר שואל שאלה של כן/לא. bool 変数名 (מתן שמות למשתנים בוליאניים, שאילתה בנתוני Search Console) משקף בדיוק את הדאגה הזו: משתנים ומתודות בוליאניות זקוקים לשמות שמבהירים את משמעותם בנקודת השימוש.

שמור על שמות יציבים בשיטות קשורות. If basePrice() משמש על ידי discountFactor(), עקביות השמות אומרת לקוראים ש discountFactor תלוי basePriceמתן שמות לא עקבי שובר את התיעוד המרומז הזה.

מתי להחיל את החלפת זמנית עם שאילתה

יש ליישם שיפוץ זה כאשר:

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

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

מתי לא להחיל החלפת זמנית בשאילתה

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

פִּיתוֹן

# Memoized property: computed once, cached
from functools import cached_property

class Order:
    @cached_property
    def expensive_validation(self):
        return self.external_service.validate(self.data)  # called once, cached

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

טמפרטורות המצטברות לאורך איטרציות לולאה. טמפרטורה שהיא המצבר ב for לולאה, total += item.price, אינו מועמד להחלפת ערך זמני בשאילתה. זהו אינו ערך נגזר; זהו מצב שנבנה לאורך איטרציות. שקול להחליף לולאה ב-Pipeline במקום זאת אם הלולאה היא הבעיה.

טכניקות רפקטורינג קשורות

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

שיטת החילוץ היא המלווה הנפוצה ביותר. החלפת שיטת Temp ב-Query מאפשרת לעיתים קרובות את שיטת החילוץ על ידי ניקוי משתנים שאחרת היו דורשים העברת פרמטרים מסורבלת בין החלק החולץ לשאר השיטה.

Inline Temp הוא ההפך מהכנסת temp: הוא מחליף temp בביטוי שלו ישירות בקוד. השתמש ב-Inline Temp כאשר ה-temp אינו מוסיף בהירות והביטוי שלו כבר קריא.

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

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

החלפת לולאה בצינור מטפל בדפוס נפוץ שבו לולאה עם צובר זמני יכולה להיות מוחלפת על ידי פעולת צינור משורשרת (map, filter, reduce), שהוא הצהרתי יותר וקל יותר לקריאה.

איך SMART TS XL תומך בעיבוד מחדש בקנה מידה גדול

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

SMART TS XL מספק ניתוח מבני חוצה קוד שהופך את המענה על שאלה זו לשיטתי. הוא מזהה היכן מתבצע אותו חישוב כמשתנה זמני במספר מיקומים, התבנית ש-Replace Temp with Query נועדה לאחד לשיטת שאילתה אחת בעלת שם. הוא עוקב אחר האופן שבו נעשה שימוש בשיטת שאילתה שעברה שחזור לאחר שהיא מחולצת, מה שהופך את היקף השחזור לגלוי לפני שהוא מבוצע. והוא עובד בשפות שונות: עבור מערכות ארגוניות שבהן תוכניות COBOL, שירותי Java וצינורות Python פועלים כולם על אותם נתונים, ניתוח קוד סטטי ו ניתוח השפעות לזהות היכן אותו חישוב לוגי מופיע בצורות שונות בשפות שונות, הצורה העמוקה יותר של הבעיה ש-Replace Temp with Query מטפלת בה ברמה של שפה אחת.

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

משתנים זמניים וקוד תיעוד עצמי

ההחלטה להחליף שאילתה זמנית (temporary) היא בסופו של דבר החלטה לגבי מה שהקוד צריך להעביר. משתנים זמניים (temporary variables communicate) מעבירים גישה לביצוע: זהו החישוב שביצעתי כדי לקבל ערך זה. שיטות שאילתה (query methods communicate domain): זוהי משמעות הערך הזה. במחלקה Order, basePrice() אומר לקורא שהמושג הזה קיים בתחום. double x = quantity * itemPrice מספר לקורא על פעולה אריתמטית.

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