איך בוחרים תבנית עיצוב בלי להתאהב בפתרון: טבלת החלטה מעשית למפתחי תוכנה
תבנית עיצוב טובה לא מתחילה בשם יפה כמו Factory Method, Decorator או Observer. היא מתחילה באי־נוחות בקוד: מחלקה שתופחת, שינוי קטן שגורר תיקונים בחמישה מקומות, תלות שקשה לבדוק, או מודול שאי אפשר להחליף בלי לשבור חצי מערכת.
הטעות הנפוצה היא לפתוח קטלוג תבניות ולשאול “איזו תבנית אפשר להכניס כאן?”. השאלה הנכונה היא הפוכה: איזו בעיית עיצוב יש כאן, אילו כוחות מתנגשים, ואיזו תבנית פותרת אותם במחיר סביר? המאמר הזה מציע דרך עבודה וטבלת החלטה שמתחילה מהבעיה, עוברת דרך עקרונות עיצוב מונחה עצמים ו־SOLID, ורק בסוף מגיעה לתבנית.
הגישה מתאימה למערכות מונוליטיות, למיקרו־שירותים, לתכנון API, וגם לקוד יומיומי שבו אין שום סיבה “לעשות ארכיטקטורה” אם הבעיה פשוטה. תבנית עיצוב היא כלי. כמו כל כלי, היא עוזרת רק כשהחומר שמולך באמת דורש אותה.
נקודת המוצא: לתאר את הכאב לפני שמחפשים שם
לפני שבוחרים תבנית, כתבו משפט אחד שמתאר מה כואב בקוד. לא “צריך כאן Strategy”, אלא “יש לנו כמה אלגוריתמים לבחירת מחיר, והם משתנים בלי קשר למחלקת ההזמנה”. לא “נעשה Factory”, אלא “הלקוח לא צריך לדעת איזו מחלקה קונקרטית נוצרת לפי סוג המסמך”.
משפט טוב כולל שלושה חלקים:
- מה משתנה: אלגוריתם, אובייקט שנוצר, מבנה היררכי, סדר פעולות, ממשק חיצוני.
- מה צריך להישאר יציב: קוד לקוח, חוזה ציבורי, מודול עסקי, בדיקות קיימות.
- מה המחיר הנוכחי: כפילות, תנאים מתרבים, תלות חזקה, קושי בבדיקות, חשש משינוי.
אם אי אפשר לנסח את הכאב, לרוב אין עדיין צורך בתבנית. ייתכן שהפתרון הוא שם טוב יותר למתודה, חילוץ מחלקה, פישוט תנאי, או מחיקה של הכללה מוקדמת. לא כל ריח קוד הוא הזמנה לתבנית GoF.
מה להכין מראש לפני ההחלטה
בחירה נכונה דורשת מעט מידע. לא מסמך כבד, אלא תמונה מינימלית של ההתנהגות והגבולות.
- תרחישי שינוי צפויים: אילו וריאציות כבר קיימות, ואילו וריאציות סבירות בחודשים הקרובים.
- מפת תלות קצרה: מי קורא למי, מי יוצר את מי, ומי מכיר טיפוסים קונקרטיים.
- גבולות אחריות: איפה נגמרת לוגיקה עסקית ומתחילה תשתית,
API, אחסון או אינטגרציה. - אילוצי בדיקות: מה קשה לבדוק היום, ומה אסור לשבור.
- שרטוט קטן אם צריך: תרשים
UMLפשוט, במיוחדuml class diagram, מספיק לעיתים כדי לראות שהמחלקות מדברות יותר מדי.
שרטוט אינו מטרה בפני עצמה. שני מלבנים וחץ יכולים להיות מועילים יותר מתרשים מפואר. אם תרשים uml diagram לא עוזר להסביר אחריות, תלות או כיוון שינוי, עדיף לוותר עליו.
טבלת החלטה: מהבעיה אל התבנית
הטבלה הבאה אינה מחליפה שיקול דעת, אבל היא עוזרת לעצור את הדחף לבחור לפי שם מוכר. התחילו בעמודת “הסימן בקוד”, בדקו את הכוחות, ואז שקלו תבנית.
| הסימן בקוד | השאלה שצריך לשאול | תבניות מועמדות | סימן אזהרה |
|---|---|---|---|
הרבה if או switch על סוג פעולה |
האם ההתנהגות משתנה בלי לשנות את הלקוח? | Strategy, לפעמים State |
אם יש רק שתי אפשרויות יציבות, ייתכן שתנאי פשוט מספיק. |
| קוד לקוח יוצר מחלקות קונקרטיות רבות | האם צריך להסתיר החלטת יצירה? | Factory Method, Abstract Factory, Builder |
אם אין וריאציה אמיתית ביצירה, מפעל רק מוסיף שכבה. |
| צריך להוסיף יכולות לאובייקט בלי ירושה עמוקה | האם יכולות מורכבות סביב אותו ממשק? | Decorator |
אם סדר העיטופים לא ברור, הדיבוג עלול להיות קשה. |
| קוד לקוח מכיר מערכת פנימית מסובכת | האם צריך ממשק פשוט לשימוש נפוץ? | Facade |
אם החזית מסתירה החלטות עסקיות חשובות, היא תהפוך לצוואר בקבוק. |
| אובייקטים רבים צריכים לדעת על שינוי באובייקט אחד | האם נדרש עדכון עקיף ולא תלות ישירה? | Observer |
אם סדר האירועים קריטי, צריך להיזהר מאפקטים נסתרים. |
| מבנה עץ של חלקים ושלמים | האם לקוח צריך לטפל ביחיד ובקבוצה באופן אחיד? | Composite |
אם אין פעולות משותפות אמיתיות, האחידות תהיה מאולצת. |
| רצף פעולות תלוי בבקשות משתנות | האם כמה מטפלים יכולים לנסות לטפל בלי שהשולח יכיר אותם? | Chain of Responsibility |
אם חובה לדעת מי טיפל, השרשרת צריכה מנגנון שקיפות. |
| מודולים רבים מתקשרים אחד עם השני ישירות | האם צריך לרכז תיאום ולהקטין צימוד? | Mediator |
אם המתווך צובר לוגיקה עסקית, הוא הופך לאובייקט ענק. |
שימו לב שהטבלה לא אומרת “יש switch, לכן חובה Strategy”. היא אומרת: יש סימן, עכשיו בדקו את הכוח העיצובי. האם השינוי באמת צפוי? האם הלקוח באמת צריך להיות יציב? האם העלות של מחלקות נוספות מוצדקת?
שלב אחר שלב: תהליך בחירה שלא מתחיל מהקטלוג
1. מצאו את ציר השינוי
ציר שינוי הוא הדבר שהמערכת מבקשת להחליף, להרחיב או לדחות להחלטה מאוחרת. בתמחור, זה יכול להיות אלגוריתם הנחה. ביצירת מסמכים, זה יכול להיות סוג אובייקט. במיקרו־שירותים, זה יכול להיות פרוטוקול או חוזה API מול שירות אחר.
אם אין ציר שינוי ברור, אל תבחרו תבנית. התחילו מריפקטורינג קטן: חילוץ מתודה, הפרדת אחריות, או הקטנת מחלקה. תבנית טובה מגנה על שינוי ידוע; היא לא קמע נגד אי־ודאות כללית.
2. בדקו מי צריך לדעת מה
שאלה פשוטה חושפת הרבה: מי חייב להכיר את המחלקה הקונקרטית? אם קוד לקוח צריך לדעת אם נוצר PdfReport או HtmlReport, אולי יש בעיית יצירה. אם הוא צריך לדעת באיזה סדר קוראים לחמישה רכיבים, אולי חסרה חזית. אם הוא צריך לדעת איך לחשב כל סוג הנחה, אולי חסרה אסטרטגיה.
כאן נכנסים עקרונות כמו Single Responsibility Principle ו־Dependency Inversion Principle. לא כדי לדקלם אותם, אלא כדי לשאול: האם המחלקה הזו משתנה בגלל יותר מסיבה אחת? האם מודול ברמה גבוהה תלוי בפרטים נמוכים מדי?
3. בחרו את הקטגוריה לפני שם התבנית
ב־GoF נהוג לחלק תבניות ליצירה, מבנה והתנהגות. החלוקה הזו שימושית מאוד בהחלטה:
- בעיה ביצירת אובייקטים: התחילו מ־תבניות יצירה. שם נמצאות משפחות כמו
Factory Method,Abstract Factoryו־Builder. - בעיה בהרכבת מחלקות ואובייקטים: בדקו תבניות מבנה, למשל
Adapter,Decorator,Compositeו־Facade. - בעיה בזרימת אחריות והתנהגות: שקלו תבניות כמו
Strategy,Observer,Command,MediatorאוVisitor.
החלוקה הזו מונעת ערבוב. אם הבעיה היא “מי יוצר את האובייקט”, אל תתחילו מ־Observer. אם הבעיה היא “איך כמה אובייקטים משתפים פעולה בזמן ריצה”, מפעל לא יפתור אותה.
4. בדקו את המחיר
כל תבנית מוסיפה שפה פנימית למערכת: ממשקים, מחלקות, קשרים ושמות. המחיר מוצדק כשהיא מקטינה שינוי עתידי או מסירה צימוד משמעותי. המחיר לא מוצדק כשהיא רק גורמת לקוד להיראות “ארכיטקטוני”.
כלל אצבע: אם קורא חדש צריך לפתוח חמישה קבצים כדי להבין פעולה שהייתה יכולה להיות מתודה אחת פשוטה, ייתכן שהתבנית הקדימה את זמנה.
דוגמה קצרה: כשהבעיה מובילה ל־Strategy
נניח שיש הזמנה, והמחיר הסופי תלוי במדיניות הנחה. המדיניות משתנה לפי לקוח, מבצע או ערוץ מכירה. אם נכניס את כל התנאים לתוך מחלקת ההזמנה, היא תשתנה בכל פעם שמדיניות חדשה תופיע. זו בעיית התנהגות משתנה, ולכן Strategy היא מועמדת טבעית.
interface DiscountPolicy {
double apply(double amount);
}
class NoDiscount implements DiscountPolicy {
public double apply(double amount) {
return amount;
}
}
class PercentageDiscount implements DiscountPolicy {
private final double percent;
PercentageDiscount(double percent) {
this.percent = percent;
}
public double apply(double amount) {
return amount * (1.0 - percent);
}
}
class Order {
private final double amount;
private final DiscountPolicy discountPolicy;
Order(double amount, DiscountPolicy discountPolicy) {
this.amount = amount;
this.discountPolicy = discountPolicy;
}
double total() {
return discountPolicy.apply(amount);
}
}
public class Main {
public static void main(String[] args) {
Order regular = new Order(100.0, new NoDiscount());
Order sale = new Order(100.0, new PercentageDiscount(0.15));
System.out.println(regular.total());
System.out.println(sale.total());
}
}בדוגמה הזו DiscountPolicy היא האסטרטגיה, NoDiscount ו־PercentageDiscount הן אסטרטגיות קונקרטיות, ו־Order היא ההקשר שמשתמש באסטרטגיה בלי לדעת את הפרטים שלה. הנקודה אינה תחביר Java, אלא ההפרדה: ההזמנה יודעת לחשב סכום כולל, אבל אינה מחזיקה את כל חוקי ההנחה האפשריים.
אם מחר מתווספת מדיניות חדשה, למשל הנחה לפי חברות, מוסיפים מחלקה שמממשת DiscountPolicy. אין צורך לשנות את Order. זה משרת את Open/Closed Principle: פתוח להרחבה, סגור לשינוי, כל עוד ציר השינוי אכן היה מדיניות ההנחה.
איך זה נראה בתכנון API ומיקרו־שירותים
במיקרו־שירותים, תבניות GoF עדיין רלוונטיות, אבל גבולות הבעיה משתנים. במקום רק מחלקות ואובייקטים, יש גם חוזי API, הודעות, זמינות, גרסאות ושירותים שמפותחים בנפרד. כאן חשוב במיוחד לא לבחור תבנית פנימית כדי לפתור בעיה שהיא בעצם בעיית גבול שירות.
למשל, אם שירות אחד מכיר את כל הפרטים הפנימיים של שלושה שירותים אחרים, ייתכן ש־Facade ברמת מודול או שער API פנימי יעזור. אבל אם הבעיה היא חוזים לא יציבים, תבנית מחלקות לא תפתור אותה. צריך לשפר את עיצוב החוזה, שמות המשאבים, גרסאות, ואחריות השירותים.
גם Observer יכול להופיע כרעיון בשילוב אירועים, אבל אסור לבלבל בין תבנית אובייקטים לבין ארכיטקטורת הודעות. בתהליך מקומי, צופה נקרא בזיכרון. במערכת מבוזרת יש כשלי רשת, כפילויות, סדר אירועים ואפשרות לעיבוד חוזר. הרעיון דומה; האילוצים שונים.
טעויות נפוצות בבחירת תבנית
בחירה לפי שם מוכר
תבניות פופולריות מקבלות שימוש יתר. Factory לכל יצירה, Strategy לכל תנאי, Decorator לכל תוספת. אם אין בעיית צימוד, וריאציה או הרחבה צפויה, השם לא מצדיק את המבנה.
ערבוב בין תבנית לבין שכבה
Facade אינה “שכבת שירות” אוטומטית. Repository אינו קסם שמכשיר כל גישה לנתונים. תבנית מתארת שיתוף פעולה בין אובייקטים; שכבה מתארת חלוקת אחריות רחבה יותר. לפעמים הן נפגשות, אבל הן לא אותו דבר.
UML שמסתיר במקום להסביר
תרשים UML טוב מבהיר מי תלוי במי. תרשים רע נותן תחושה שהעיצוב “רשמי” גם כשהאחריות לא ברורה. אם משתמשים בתרשים, כדאי לציין ליד כל מחלקה משפט אחריות אחד. בלי זה, מלבנים וחצים לא מספיקים.
הכללה מוקדמת
הפיתוי הגדול הוא להכין מערכת “גמישה לכל דבר”. בפועל, גמישות כללית מדי יוצרת קוד שקשה לקרוא. עדיף להגן על שינוי ממשי אחד מאשר לבנות מנגנון מופשט לחמישה שינויים דמיוניים.
מתי לא להשתמש בתבנית עיצוב
יש מצבים שבהם התשובה המקצועית היא לא להשתמש בתבנית:
- כאשר יש מימוש אחד בלבד ואין סיבה ממשית לצפות לאחר.
- כאשר התנאי פשוט, מקומי וקל לבדיקה.
- כאשר הצוות לא מרוויח בהירות מהשמות החדשים.
- כאשר התבנית תגרום ליותר קבצים, יותר עקיפות ופחות הבנה.
- כאשר הבעיה היא בגבול מערכת, חוזה
APIאו חלוקת שירותים, ולא בשיתוף פעולה בין אובייקטים.
תבנית טובה מורידה עומס קוגניטיבי לאורך זמן. אם היא מוסיפה עומס כבר ביום הראשון ואין שינוי אמיתי שהיא מגינה עליו, עדיף להתחיל פשוט יותר.
מה עושים כשנתקעים בין שתי תבניות
לעיתים שתי תבניות נראות מתאימות. Strategy ו־State דומות במבנה, אבל הכוונה שונה: בראשונה מחליפים אלגוריתם; בשנייה ההתנהגות משתנה לפי מצב פנימי. Decorator ו־Composite משתמשות בממשק משותף, אבל הראשונה מוסיפה אחריות לאובייקט יחיד, והשנייה מייצגת חלקים ושלם.
במצב כזה שאלו:
- מהו שם השינוי העסקי? אם זה “מדיניות”, לרוב זו אסטרטגיה. אם זה “מצב”, בדקו
State. - האם אנחנו יוצרים, מרכיבים או מתאמים התנהגות?
- איזו תבנית תשאיר את קוד הלקוח יציב יותר?
- איזו תבנית תהיה קלה יותר למחיקה אם נגלה שטעינו?
השאלה האחרונה חשובה במיוחד. עיצוב טוב אינו רק כזה שקל להרחיב; הוא גם כזה שאפשר לסגת ממנו בלי נזק גדול.
סיכום: תבנית היא תשובה, לא נקודת פתיחה
בחירת תבנית עיצוב מתחילה מאבחון: מה משתנה, מה חייב להישאר יציב, מי יודע יותר מדי, ואיזה מחיר אנחנו משלמים היום. רק אחרי זה כדאי לפתוח את קטלוג התבניות של GoF, או לעיין בחלוקה של תבניות מבנה ותבניות אחרות.
הטבלה במאמר אינה נוסחה מכנית. היא דרך לשאול שאלות טובות יותר. אם הבעיה היא יצירה, חשבו על תבניות יצירה. אם הבעיה היא הרכבה, חשבו על מבנה. אם הבעיה היא שיתוף פעולה והתנהגות משתנה, חשבו על תבניות התנהגות. ובעיקר: אל תתחילו מהתבנית. התחילו מהכאב בקוד.


