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

תבנית Factory Method: הגדרה, מבנה, מימוש מלא ב-Java ובפייתון, ומתי היא עדיפה על new ישיר

Factory Method מגדירה ממשק ליצירת אובייקט ומשאירה למחלקות־הבת להחליט איזו מחלקה ליצור. ההגדרה המדויקת, ארבעת המשתתפים, מימוש מלא ב-Java ובפייתון, ההבדל מ"פקטורי פשוט" ומתי new ישיר עדיף.

כל קריאה ל-new היא החלטה: באיזו מחלקה בדיוק להשתמש. כל עוד ההחלטה הזו יושבת בקוד שמשתמש באובייקט, הקוד קשור למחלקה הקונקרטית, וכל החלפה שלה מחייבת לפתוח אותו מחדש. תבנית Factory Method — שבחיפושים מופיעה לרוב בשם הכללי factory design pattern — מוציאה את ההחלטה הזו למקום אחד ומוגדר: מתודה שמחלקות־בת דורסות. המאמר מציג את ההגדרה המדויקת שלה, את המשתתפים, מימוש מלא ב-Java ומקבילה בפייתון, ואת המקרים שבהם עדיף להישאר עם new פשוט.

הכוונה

ספר ה-Gang of Four מגדיר את הכוונה של Factory Method כך: להגדיר ממשק ליצירת אובייקט, אבל לתת למחלקות־הבת להחליט איזו מחלקה ליצור. Factory Method מאפשרת למחלקה לדחות את יצירת המופע למחלקות־הבת שלה. התבנית שייכת למשפחת תבניות היצירה, והיא תבנית ברמת המחלקה: הגמישות שלה מגיעה מירושה, לא מהרכבת אובייקטים בזמן ריצה. השם החלופי שלה בספר הוא Virtual Constructor — בנאי וירטואלי.

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

הבעיה: קוד עסקי שיודע יותר מדי

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

Java
// Before: the business logic decides the concrete class itself.
String notifyUser(String channel, String user, String text) {
    if (channel.equals("email")) {
        return new EmailNotification().deliver(user, text.trim());
    } else if (channel.equals("sms")) {
        return new SmsNotification().deliver(user, text.trim());
    }
    throw new IllegalArgumentException("unknown channel: " + channel);
}

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

המשתתפים והמבנה

בתרשים המחלקות של התבנית יש שתי היררכיות מקבילות. משמאל, ממשק Product ומתחתיו המימושים הקונקרטיים. מימין, מחלקה מופשטת Creator שמצהירה על מתודה מופשטת בשם factoryMethod() שמחזירה Product, ולצידה מתודות רגילות שקוראות לה. מתחת ל-Creator יש מחלקות־בת קונקרטיות, וכל אחת דורסת את factoryMethod() ומחזירה מופע של מוצר קונקרטי אחד. חץ תלות יוצא מכל ConcreteCreator אל ה-ConcreteProduct שהוא יוצר — וזה החץ היחיד בתרשים שמגיע למחלקה קונקרטית.

משתתף תפקיד בדוגמה שלנו
Product הממשק של האובייקטים שהמתודה יוצרת Notification
ConcreteProduct מימוש קונקרטי של הממשק EmailNotification, SmsNotification
Creator מצהיר על מתודת היצירה ומכיל את הלוגיקה שמשתמשת במוצר. יכול לספק מימוש ברירת מחדל Notifier
ConcreteCreator דורס את מתודת היצירה ומחזיר מוצר קונקרטי EmailNotifier, SmsNotifier

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

מימוש מלא ב-Java

הקוד הבא הוא קובץ אחד שלם. שמרו אותו בשם Main.java, קמפלו והריצו:

Java
import java.util.List;

// Product: the interface of the objects the factory method creates.
interface Notification {
    String deliver(String recipient, String text);
}

// Concrete products.
class EmailNotification implements Notification {
    @Override
    public String deliver(String recipient, String text) {
        return "email to " + recipient + "@example.com: " + text;
    }
}

class SmsNotification implements Notification {
    @Override
    public String deliver(String recipient, String text) {
        String shortText = text.length() > 20 ? text.substring(0, 20) + "..." : text;
        return "sms to " + recipient + ": " + shortText;
    }
}

// Creator: holds the shared logic and declares the factory method.
abstract class Notifier {
    // The factory method: subclasses decide which Notification to create.
    protected abstract Notification createNotification();

    // Shared logic that uses the product without knowing its concrete class.
    public String notifyUser(String user, String text) {
        Notification notification = createNotification();
        return notification.deliver(user, text.trim());
    }
}

// Concrete creators: each one overrides the factory method.
class EmailNotifier extends Notifier {
    @Override
    protected Notification createNotification() {
        return new EmailNotification();
    }
}

class SmsNotifier extends Notifier {
    @Override
    protected Notification createNotification() {
        return new SmsNotification();
    }
}

public class Main {
    public static void main(String[] args) {
        List<Notifier> notifiers = List.of(new EmailNotifier(), new SmsNotifier());
        for (Notifier notifier : notifiers) {
            System.out.println(notifier.notifyUser("dana", "  Your order #1042 has shipped  "));
        }
    }
}

הפלט:

פלט
email to [email protected]: Your order #1042 has shipped
sms to dana: Your order #1042 has...

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

אותו מבנה בפייתון

Python
from abc import ABC, abstractmethod


class Notification(ABC):
    @abstractmethod
    def deliver(self, recipient, text): ...


class EmailNotification(Notification):
    def deliver(self, recipient, text):
        return f"email to {recipient}@example.com: {text}"


class SmsNotification(Notification):
    def deliver(self, recipient, text):
        short = text if len(text) <= 20 else text[:20] + "..."
        return f"sms to {recipient}: {short}"


class Notifier(ABC):
    @abstractmethod
    def create_notification(self) -> Notification: ...

    def notify_user(self, user, text):
        return self.create_notification().deliver(user, text.strip())


class EmailNotifier(Notifier):
    def create_notification(self):
        return EmailNotification()


class SmsNotifier(Notifier):
    def create_notification(self):
        return SmsNotification()


for notifier in (EmailNotifier(), SmsNotifier()):
    print(notifier.notify_user("dana", "  Your order #1042 has shipped  "))

בפייתון מחלקות הן ערכים, ולכן אפשר היה גם להעביר ל-Notifier את מחלקת המוצר כפרמטר ולוותר על מחלקות־הבת. זו כבר הרכבה ולא ירושה, והיא קרובה יותר לרוח של Abstract Factory או של Prototype. הגרסה שלמעלה שומרת על המבנה של ה-GoF כדי שההשוואה תהיה ישירה.

איך מגיעים לתבנית מקוד קיים

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

  1. מחלצים ממשק למוצר. כל המחלקות שנוצרות ב-if מקבלות ממשק משותף, והמשתנה המקומי מוגדר לפי הממשק ולא לפי המחלקה.
  2. מעבירים את היצירה למתודה נפרדת. ה-if עובר כמו שהוא למתודה מוגנת בשם createNotification, והלוגיקה העסקית קוראת לה.
  3. מפצלים את המתודה למחלקות־בת. כל ענף של ה-if הופך למימוש של המתודה במחלקת־בת משלו, ובמחלקת הבסיס המתודה נעשית מופשטת.
  4. מעבירים את הבחירה לקצה. את ההחלטה איזה Notifier ליצור מקבלים פעם אחת, במקום שבו המערכת מורכבת — בדרך כלל בנקודת הכניסה או בהגדרות — ולא בכל מתודה מחדש.

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

שלושה דברים שנקראים "פקטורי"

המילה factory משמשת לשלושה דברים שונים, וחלק גדול מהבלבול סביב התבנית נובע מזה:

שם מה זה מקור
Factory Method מתודה במחלקת בסיס שמחלקות־בת דורסות כדי להחליט מה ליצור ספר ה-GoF
Abstract Factory אובייקט שיוצר משפחה של מוצרים קשורים, ומועבר לקוד כפרמטר ספר ה-GoF
Simple Factory מחלקה או מתודה סטטית אחת עם switch שמחזירה מוצר לפי פרמטר טכניקה נפוצה, לא תבנית בספר

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

וריאציות מוכרות

  • מימוש ברירת מחדל. ה-Creator לא חייב להיות מופשט. הוא יכול להחזיר מוצר סביר כברירת מחדל, ומחלקות־בת ידרסו רק כשצריך משהו אחר.
  • מתודת יצירה עם פרמטר. הספר מתאר גם וריאציה שבה מתודת היצירה מקבלת מזהה ומחזירה מוצרים שונים לפיו. מחלקת־בת יכולה להרחיב את הבחירה ולהוסיף מזהים חדשים, ולהעביר את השאר ל-super.
  • היררכיות מקבילות. כשמחלקה מעבירה חלק מהאחריות שלה לאובייקט נלווה, Factory Method מחברת בין ההיררכיה של המחלקה להיררכיה של הנלווים. דוגמה מוכרת מהספרייה הסטנדרטית של Java: המתודה iterator() של Collection. כל סוג אוסף מחזיר Iterator משלו, והקוד שעובר על האוסף אינו יודע איזה.

השלכות

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

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

מתי לא להשתמש

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

תבניות קשורות

Abstract Factory ממומשת לעיתים קרובות בעזרת כמה Factory Methods, אחת לכל מוצר במשפחה. Template Method קשורה בקשר הדוק: בדרך כלל מתודת היצירה נקראת מתוך מתודת תבנית, בדיוק כמו ש-notifyUser קוראת ל-createNotification. Prototype היא חלופה שאינה דורשת מחלקת־בת ל-Creator, אבל דורשת פעולת אתחול על המוצר. התמונה הכללית של משפחת היצירה נמצאת במבוא לתבניות עיצוב.

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

  • לקרוא ל"פקטורי פשוט" Factory Method. מתודה סטטית עם switch שמחזירה אובייקט לפי מחרוזת היא טכניקה לגיטימית שמרכזת את ההחלטה במקום אחד, אבל אין בה ירושה ואין בה מחלקות־בת שמחליטות. גם "static factory method" — מתודה סטטית כמו List.of שמחליפה בנאי — אינה התבנית מהספר, למרות השם הדומה.
  • לקרוא למתודת היצירה מתוך הבנאי של ה-Creator. ב-Java, כשבנאי של מחלקת בסיס קורא למתודה שנדרסה, המתודה רצה לפני ששדות מחלקת־הבת אותחלו. אם המוצר תלוי בשדות האלה, תקבלו null. עדיף ליצור את המוצר בעצלות, ברגע שהוא נדרש.
  • להחזיר טיפוס קונקרטי. אם החתימה של מתודת היצירה מחזירה EmailNotification ולא Notification, הקוד הקורא נקשר שוב למחלקה הקונקרטית, וכל התועלת אבדה.
  • ליצור היררכיית Creators בשביל מוצר אחד. זה הקוד שנותן לתבניות שם רע: שלוש מחלקות ושני ממשקים סביב קריאה אחת ל-new.

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

נכתב על ידי

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

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

להמשך קריאה

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

תבניות יצירה

singleton design pattern: תבנית Singleton, מימוש נכון, בטיחות בין חוטים, ולמה רבים רואים בה אנטי־תבנית

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

8 דק׳ קריאה

תבניות יצירה

תבנית Builder: בניית אובייקט מורכב צעד אחר צעד, מימוש מלא, Fluent Builder ו-Director

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

9 דק׳ קריאה

תבניות יצירה

תבנית Abstract Factory: משפחות של אובייקטים קשורים, מימוש מלא וההבדל מ-Factory Method

Abstract Factory מספקת ממשק ליצירת משפחות של אובייקטים קשורים בלי לציין את המחלקות הקונקרטיות שלהם. ההגדרה לפי ספר ה-GoF, חמשת המשתתפים, מימוש מלא ב-Java ובפייתון, ומה ההבדל האמיתי בינה לבין Factory Method.

8 דק׳ קריאה