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

מה הן תבניות עיצוב (Design Patterns)? מקור, שלוש המשפחות, ומתי תבנית היא סתם שכבה מיותרת

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

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

הגדרה: פתרון בעל שם לבעיה שחוזרת

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

ספר ה-Gang of Four, שעליו נרחיב בהמשך, מגדיר ארבעה מרכיבים שכל תבנית חייבת לכלול:

  1. שם. מילה או שתיים שמאפשרות לדבר על הפתרון בלי לתאר אותו מחדש. זה נשמע כמו פרט שולי, אבל זה חצי מהערך: "בוא נעטוף את זה ב-Decorator" הוא משפט שמחליף פסקה שלמה.
  2. בעיה. מתי התבנית רלוונטית — אילו תנאים, אילו סימפטומים בקוד, אילו כוחות מושכים לכיוונים שונים.
  3. פתרון. המבנה: המשתתפים, היחסים ביניהם והאחריות של כל אחד. מופשט מספיק כדי להתאים להרבה מקרים.
  4. השלכות. מה מרוויחים ומה משלמים. המרכיב הזה הוא הכי נשכח, והכי חשוב: תבנית בלי מחיר לא קיימת.

מאיפה זה הגיע

הרעיון לא נולד בתוכנה. האדריכל כריסטופר אלכסנדר תיאר בשנות השבעים "שפת תבניות" לתכנון ערים ובניינים: פתרונות שחוזרים — חלון במקום שבו יושבים, מעבר בין מרחב ציבורי לפרטי — שכל אחד מהם מתאר בעיה, את ההקשר שלה ואת הפתרון. בסוף שנות השמונים החלו מפתחים בעולם התכנות מונחה העצמים לאמץ את אותה צורת חשיבה, ובשנת 1994 יצא הספר שהפך אותה לזרם המרכזי: Design Patterns: Elements of Reusable Object-Oriented Software, של אריך גאמה, ריצ'רד הלם, ראלף ג'ונסון וג'ון וליסידס. ארבעת המחברים זכו לכינוי Gang of Four, ובקיצור GoF, והספר מכונה עד היום "ספר ה-GoF". על הספר עצמו, ומה התיישן בו, יש מאמר נפרד.

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

שלוש המשפחות

ה-GoF מחלק את 23 התבניות לשלוש משפחות לפי המטרה שלהן:

משפחה השאלה שהיא עונה עליה התבניות
יצירה (Creational) מי מחליט איזה אובייקט נוצר, ואיך מסתירים את ההחלטה מהקוד שמשתמש בו Abstract Factory, Builder, Factory Method, Prototype, Singleton
מבנה (Structural) איך מרכיבים מחלקות ואובייקטים למבנים גדולים יותר Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
התנהגות (Behavioral) איך מחלקים אחריות ואלגוריתמים בין אובייקטים, ואיך הם מתקשרים Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

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

באתר הזה חילקנו את משפחת ההתנהגות לשתי קטגוריות. תבניות כמו Mediator, Chain of Responsibility, Observer ו-Command מסווגות בספר כתבניות התנהגות, ונשארות כאלה; ריכזנו אותן תחת "תקשורת בין אובייקטים" רק כי השאלה המשותפת שלהן היא איך אובייקטים מעבירים הודעות בלי להכיר זה את זה.

דוגמה: אותה בעיה, לפני ואחרי

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

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

Java
import java.util.List;

// The strategy interface: one way to compute a shipping fee.
interface ShippingPolicy {
    double fee(double weightKg);
}

class StandardShipping implements ShippingPolicy {
    public double fee(double weightKg) { return 20 + 4 * weightKg; }
}

class ExpressShipping implements ShippingPolicy {
    public double fee(double weightKg) { return 45 + 6 * weightKg; }
}

class StorePickup implements ShippingPolicy {
    public double fee(double weightKg) { return 0; }
}

// The context: it uses a policy without knowing which one it is.
class Checkout {
    private final ShippingPolicy policy;

    Checkout(ShippingPolicy policy) { this.policy = policy; }

    double total(double itemsPrice, double weightKg) {
        return itemsPrice + policy.fee(weightKg);
    }
}

public class Main {
    public static void main(String[] args) {
        List<ShippingPolicy> policies =
            List.of(new StandardShipping(), new ExpressShipping(), new StorePickup());
        for (ShippingPolicy p : policies) {
            Checkout checkout = new Checkout(p);
            System.out.println(p.getClass().getSimpleName() + ": " + checkout.total(100, 2.5));
        }
    }
}

הפלט הוא StandardShipping: 130.0, ExpressShipping: 160.0 ו-StorePickup: 100.0. שיטת משלוח חדשה היא מחלקה חדשה, ו-Checkout לא משתנה בכלל.

ועכשיו אותו רעיון בפייתון, שבה פונקציות הן ערכים לכל דבר:

Python
def standard(weight_kg):
    return 20 + 4 * weight_kg

def express(weight_kg):
    return 45 + 6 * weight_kg

def pickup(weight_kg):
    return 0

def total(items_price, weight_kg, policy):
    return items_price + policy(weight_kg)

for policy in (standard, express, pickup):
    print(policy.__name__, total(100, 2.5, policy))

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

מתי תבנית עוזרת

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

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

מתי תבנית היא סתם שכבה מיותרת

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

  • יש רק מימוש אחד, ולא צפוי שני. ממשק ShippingPolicy עם מחלקה יחידה הוא לא תכנון גמיש — הוא הפניה מיותרת.
  • התבנית נבחרה לפני הבעיה. מי שקרא על Visitor אתמול ומחפש היום איפה להשתמש בו, ימצא — ויסבך קוד שלא היה צריך את זה. בחירת תבנית מתחילה מהבעיה, לא מהרשימה.
  • השפה כבר פותרת את זה. Iterator ב-Java מודרנית הוא לולאת for, ו-Command בפייתון הוא לעיתים קרובות פונקציה שנשמרת ברשימה.
  • שכבות על שכבות. Factory שמחזיר Builder שמקבל Strategy — כל אחד מוצדק לבד, ויחד הם קוד שאף אחד לא מבין. כשתבנית חוזרת בכל מקום בלי סיבה, היא כבר קרובה יותר לאנטי־תבנית.

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

תבניות ו-SOLID

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

תבניות מחוץ לספר

23 התבניות של ה-GoF הן רק הקטלוג הראשון והמפורסם. הן עוסקות ברמה של מחלקות ואובייקטים בתוך תוכנית אחת. מעליהן יש תבניות ארכיטקטורה, שעוסקות בחלוקה של מערכת שלמה: Model-View-Controller, שכבות, Repository. ומעל אלה יש תבניות של מערכות מבוזרות, שנולדו מתוך הבעיות של שירותים שמדברים זה עם זה ברשת — Saga לטרנזקציות שמתפרסות על כמה שירותים, ו-Circuit Breaker לשירות שמפסיק לפנות לשירות אחר שכבר נכשל. הצורה זהה בכל הרמות: בעיה שחוזרת, פתרון בעל שם, ומחיר שצריך לשלם. מה שמשתנה הוא קנה המידה — וככל שהוא גדל, גם הטעות בבחירת תבנית עולה יותר, כי קשה הרבה יותר לבטל אותה.

איך ללמוד אותן בלי לשנן

אין טעם ללמוד את 23 התבניות לפי סדר. חלקן נדירות מאוד בקוד יומיומי — Interpreter ו-Flyweight, למשל — וחלקן מופיעות בכל מערכת. סדר מעשי יותר:

  1. התחילו מהתבניות שכבר משתמשים בהן סביבכם בלי לקרוא להן בשם: Factory Method, Strategy, Observer ו-Decorator. רוב הספריות הסטנדרטיות מלאות בהן.
  2. אחר כך תבניות המבנה, שקל לזהות בקוד קיים: Composite, Adapter ו-Facade.
  3. בסוף — התבניות שפותרות בעיות מורכבות יותר ודורשות יותר הבנה כדי להשתמש בהן נכון: Visitor, Mediator ו-Chain of Responsibility.

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

טעויות נפוצות

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

המאמרים באתר מכסים את התבניות אחת־אחת, עם מימוש מלא ועם סעיף קבוע של "מתי לא להשתמש". אם אתם יודעים מה הבעיה אבל לא איזו תבנית מתאימה, התחילו מהטבלה בעמוד הבית.

נכתב על ידי

רועי פרידמן : כותב

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

להמשך קריאה

לכל המאמרים בנושא

מבוא לתבניות עיצוב

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

תבנית עיצוב טובה לא מתחילה בשם יפה כמו Factory Method, Decorator או Observer. היא מתחילה באי־נוחות בקוד: מחלקה שתופחת, שינוי קטן שגורר תיקונים בחמישה מקומות, תלות שקשה לבדוק, או מודול…

9 דק׳ קריאה

מבוא לתבניות עיצוב

אנטי־תבניות בתכנון תוכנה: איך מזהים God Object, Spaghetti Code ו־Golden Hammer — ואיך יוצאים מהן

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

9 דק׳ קריאה