תבנית 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 במקום אחר, ובעתיד אולי התראת אפליקציה. הגרסה הראשונה, שכמעט כולם כותבים, נראית כך:
// 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, קמפלו והריצו:
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 לא מזכירה אף מחלקה קונקרטית. ערוץ חדש — נניח התראת אפליקציה — הוא שתי מחלקות חדשות, מוצר ויוצר, בלי לגעת בשורה אחת של הקוד הקיים. זה עקרון פתוח/סגור בפעולה.
אותו מבנה בפייתון
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 שהוצגה למעלה, ברגע שהיא מתחילה להכאיב. המעבר עצמו מכני, ואפשר לעשות אותו בארבעה צעדים קטנים שכל אחד מהם משאיר את הקוד עובד:
- מחלצים ממשק למוצר. כל המחלקות שנוצרות ב-
ifמקבלות ממשק משותף, והמשתנה המקומי מוגדר לפי הממשק ולא לפי המחלקה. - מעבירים את היצירה למתודה נפרדת. ה-
ifעובר כמו שהוא למתודה מוגנת בשםcreateNotification, והלוגיקה העסקית קוראת לה. - מפצלים את המתודה למחלקות־בת. כל ענף של ה-
ifהופך למימוש של המתודה במחלקת־בת משלו, ובמחלקת הבסיס המתודה נעשית מופשטת. - מעבירים את הבחירה לקצה. את ההחלטה איזה
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 פשוט לעשות את העבודה בכל שאר המקרים.


