תבניות יצירה בתכנות - הסבר ודוגמאות¶
בפוסט הקודם דיברנו על תבניות עיצוב באופן כללי, ועכשיו הגיע הזמן לצלול לקטגוריה הראשונה - תבניות יצירה. אלה תבניות שעונות על שאלה פשוטה כביכול - איך יוצרים אובייקט? אבל כשהיצירה הזאת מסובכת, גמישה, או צריכה לעמוד בכללים מסוימים, פתאום השאלה הזאת פחות טריוויאלית ממה שנשמע.
Factory - כשיצירת האובייקט תלויה בתנאי¶
תבנית Factory פותרת מצב שבו אתם צריכים ליצור אובייקט, אבל הסוג המדויק שלו נקבע רק בזמן ריצה. במקום שהקוד שלכם יקרא ישירות ל-new CreditCardPayment() או new PayPalPayment() בהתאם לתנאי, יש פונקציה - "מפעל" - שמקבלת פרמטר ומחזירה את האובייקט הנכון.
class PaymentFactory:
@staticmethod
def create(payment_type):
if payment_type == "credit_card":
return CreditCardPayment()
elif payment_type == "paypal":
return PayPalPayment()
raise ValueError("Unknown payment type")
היתרון - הקוד שקורא ל-PaymentFactory.create(...) לא צריך לדעת שום דבר על המחלקות הקונקרטיות שקיימות. אם מוסיפים ספק תשלום חדש, משנים רק את המפעל, לא כל מקום בקוד שיוצר תשלום.
Builder - כשליצירת האובייקט יש הרבה שלבים ואפשרויות¶
תבנית Builder מתאימה כשיצירת אובייקט דורשת הרבה פרמטרים אופציונליים, ובנאי (constructor) רגיל עם עשרה פרמטרים הופך לבלתי קריא. תארו לעצמכם אובייקט HttpRequest שיכול לכלול כתובת, headers, גוף בקשה, timeout, ועוד עשרה שדות אופציונליים.
במקום קונסטרוקטור ענק, Builder מאפשר לבנות את האובייקט צעד אחר צעד, בצורה קריאה:
request = (
RequestBuilder()
.url("https://api.example.com")
.header("Authorization", "Bearer token")
.timeout(30)
.build()
)
כל מתודה מחזירה את ה-builder עצמו, מה שמאפשר לשרשר קריאות בצורה נוחה, ורק ה-build() בסוף בפועל יוצר את האובייקט הסופי. זה הופך קוד שהיה עלול להיראות כמו קונסטרוקטור עם עשרה ארגומנטים במיקום נכון, למשהו שקריא בבירור מה כל ערך מייצג.
Singleton - כשצריך שיהיה בדיוק מופע אחד¶
תבנית Singleton מבטיחה שלמחלקה מסוימת יהיה בדיוק מופע אחד בכל התוכנית, עם נקודת גישה גלובלית אליו. הדוגמה הקלאסית היא חיבור למסד נתונים, או אובייקט קונפיגורציה גלובלי - אתם לא רוצים ליצור עשרים חיבורים נפרדים למסד הנתונים, אתם רוצים אחד משותף שכולם משתמשים בו.
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
חשוב לומר בכנות - Singleton הוא התבנית השנויה ביותר במחלוקת מבין כל תבניות היצירה, ולא בכדי. הוא בעצם יוצר מצב גלובלי (global state) בתוכנית, מה שהופך בדיקות (טסטים) לקשות יותר - כי אי אפשר בקלות "לאפס" את המצב בין טסט לטסט - ויוצר תלות סמויה שקשה לעקוב אחריה. בהרבה מקרים, מה שנראה כמו צורך ב-Singleton בעצם פתיר טוב יותר עם הזרקת תלויות (dependency injection) - להעביר את המופע היחיד כפרמטר במקום לגשת אליו גלובלית מכל מקום בקוד.
Abstract Factory - כשצריך משפחות שלמות של אובייקטים תואמים¶
זו תבנית פחות נפוצה אבל שימושית במקרה ספציפי - כשצריך ליצור קבוצות של אובייקטים שצריכים להתאים אחד לשני. דוגמה קלאסית היא ממשק משתמש שתומך בכמה ערכות עיצוב (themes) - כשבוחרים ערכת נושא כהה, צריך שכל האלמנטים - כפתורים, חלונות, תפריטים - ייווצרו בגרסת הכהה שלהם, לא שילוב מקרי של כהה ובהיר. Abstract Factory מבטיחה שכל האובייקטים שנוצרים ביחד שייכים לאותה "משפחה" עקבית.
איך יודעים איזו תבנית מתאימה¶
הכלל הפשוט - אל תתחילו מהתבנית, תתחילו מהבעיה. אם יצירת אובייקט שלכם תלויה בתנאי - חשבו על Factory. אם יש הרבה פרמטרים אופציונליים שהופכים קונסטרוקטור לבלתי קריא - חשבו על Builder. אם אתם בטוחים שצריך רק מופע אחד גלובלי - קודם תשאלו את עצמכם אם הזרקת תלויות לא פותרת את זה בצורה נקייה יותר.
תבניות היצירה הן רק חלק אחד מהתמונה - יש עוד תבניות מבנה והתנהגות שמשלימות אותן, ואנחנו עוברים על כולן לעומק, עם דוגמאות קוד מלאות, בקורס ארכיטקטורת תוכנה.
יש לכם דילמה עיצובית ספציפית ולא בטוחים איזו תבנית מתאימה? שאלו את הקהילה בדיסקורד, לפעמים זוג עיניים חיצוני רואה את הפתרון מהר יותר.
לסיכום¶
תבניות יצירה עונות על שאלה אחת - איך בונים אובייקטים בצורה גמישה במקום נוקשה. Factory מתאים כשהסוג נקבע בזמן ריצה, Builder כשיש הרבה פרמטרים אופציונליים, ו-Singleton כשבאמת צריך מופע גלובלי יחיד - אם כי כדאי לבדוק תמיד אם הזרקת תלויות לא פתרון נקי יותר. כמו תמיד, השאלה המנחה היא לא "איזו תבנית מגניבה", אלא "איזו בעיה יש לי באמת".