|
|
|
כמה נקודות בקשר למימוש:
- שתי ווריאציות עיקריות - שתי ווריאציות עיקריות של ה factory method pattern הן (1) המקרה שבו ה Creator הוא
abstract class ואינו מספק את המימוש ל factory method שמוכרזת אצלו, ו (2) המקרה בו ה Creator הוא
concrete class ומספק מימוש ברירת מחדל ל factory method. ישנה אפשרות שלישית בה ה Creator הוא
abstract class ולמרות זה הוא מגדיר מימוש ברירת מחדל ל factory method אבל היא פחות נפוצה.
המקרה הראשון דורש שה subclass ים יגדירו מימוש ל factory method מכיוון אין בו מימוש שלה. מקרה זה פותר את הבעיה של יצירת class
ים מסוג שאינו ידוע מראש, בעיה שהוזכרה בסעיף קודם. במקרה השני, ה concrete Creator משתמש ב factory method בעיקר למען הגמישות והאפשרות לשינויים עתידיים. הוא עובד לפי חוק האומר: "צור אובייקטים בפעולה נפרדת, כדי ש subclass ים יוכלו לדרוס ולשפר אותה בעתיד". חוק זה מבטיח את האפשרות שמעצבי ה subclass ים יוכלו לשנות את מחלקות האובייקטים הנוצרות על ידי ה parent classes שלהם.
- Factory methods בעלות פרמטרים - ווריאציה אחרת של ה pattern מאפשרת ל factory method ליצור סוגים שונים של products. ה factory method מקבלת פרמטר הקובע איזה סוג אובייקט היא צריכה ליצור. כל האובייקטים שהיא תייצר חולקים את ממשק משותף של
Product . בדוגמה עם ה Document, אפשר ש Application תתמוך במספר סוגים של Document. ניתן יהיה להעביר ל
CreateDocument() פרמטר נוסף אשר יקבע את סוג המסמך.
ה framework הגרפית Unidraw משתמשת בגישה זו כדי לבנות מחדש אובייקטים שנשמרו על דיסק. היא מגדירה מחלקת
Creator עם factory method שנקראת Create() () ואשר מקבלת כארגומנט שדה
ProductId שקובע איזה סוג class ליצור. כאשר Unidraw שומרת אובייקט לקובץ היא כותבת קודם כל לדיסק את ה
ProductId שלו ולאחר מכן את האובייקט עצמו. כאשר היא בונה מחדש את האובייקט מהדיסק היא קוראת את
ProductId תחילה. לאחר מכן מתבצעת קריאה לפונקציה
Create() עם הפרמטר
ProductId ואז, לפי הפרמטר, Create()
קוראת ל constructor הנכון ומשתמשת בו כדי ליצור מופע של אובייקט מסוג זה. לבסוף, נקראים נתוני האובייקט מהדיסק ומאותחלים לתוך האובייקט שנוצר בעזרת הפונקציה
Read(). ל factory method בעלת פרמטרים יש את הצורה הכללית הבאה, כאשר
MyProduct וYourProduct הם subclass ים של
Product:
class Creator {
public:
virtual Product* Create(ProductId);
};
Product* Creator::Create(ProductId id) {
if (id == MINE) return new MyProduct;
if (id == YOURS) return new YourProduct;
// repeat for remaining products...
return 0;
}
דריסה של factory method בעלת פרמטרים מאפשרת לנו בקלות להרחיב או לשנות את ה products ש
Creator מייצר. אפשר להוסיף ProductId
ים חדשים כדי לתמוך בסוגים חדשים של products, ואפשר לקשר ProductId
ים קיימים עם סוגי אובייקטים אחרים. למשל, אפשר לממש subclass שיקרא MyCreator
כך שיחליף בין ה products שהוא מייצר ויתמוך גם באחד חדש, TheirProduct:
Product* MyCreator::Create(ProductId id) {
if (id == YOURS) return new MyProduct;
if (id ==MINE) return new YourProduct;
// N.B.: switched YOURS and MINE
if (id == THEIRS) return new TheirProduct;
return Creator::Create(id); // called if
// all others fail
}
שימו לב שהשורה האחרונה קוראת ל Create() של ה parent class. זאת מכיוון ש MyCreator::Create() משנה רק שלושה מקרים שהם
MINE, YOURS, ו THEIRS וכל שאר המקרים מטופלים כמו ב parent class. לכן
MyCreator מרחיב את סוגי המוצרים הנוצרים, ועדיין משאיר את האחריות ליצירת רב האובייקטים ל parent שלו.
- נושאים שספציפיים ל ++C - ב ++C היא תמיד פונקציה ווירטואלית ולעתים קרובות היא pure virtual. היזהרו לא לקרוא ל factory methods ב constructor של Creator, כי ה factory method של ה ConcreteCreator לא תהיה זמינה עדיין.
אפשר להימנע מכך על ידי כך שניכנס ל product רק דרך accessor operations אשר יוצרות את ה product על פי דרישה. מה זה אומר: במקום ליצור את ה concrete product ב constructor ה constructor רק מאתחל אותו ל 0; מי שיוצר את ה product הוא ה accessor, זאת לאחר שהוא בודק שהוא אינו קיים עדיין. ומתי הוא יוצר אותו? כאשר מישהו רוצה לבצע עליו פעולה. טכניקה זו קרויה
Lazy initialization והקוד הבא מדגים זאת:
class Creator {
public:
Product* GetProduct();
protected:
virtual Product* CreateProduct();
private:
Product* _product;
};
Product* Creator::GetProduct() {
if (_product == 0) {
_product = CreateProduct();
}
return _product;
}
- שימוש ב templates כדי להימנע מ subclassing - כפי שצוין בסעיף קודם, אחד החסרונות של ה factory method הוא שהיא עלולה להכריח אותנו לבצע subclassing רק כדי ליצור את ה product המתאים. דרך אחת להסתדר עם זה ב ++C היא לספק subclass של Creator שהוא template והפרמטר שלו יהיה ה Product:
class Creator {
public:
virtual Product* CreateProduct() = 0;
};
template <class TheProduct>
class StandardCreator : public Creator {
public:
virtual Product* CreateProduct();
};
template <class TheProduct>
Product* StandartCreator<TheProduct>::CreateProduct() {
return new TheProduct;
}
עם ה template הזו הקליינט מספק רק את המחלקה Product ואין צורך ב subclassing של Creator:
class MyProduct : public Product {
public:
MyProduct();
// ...
};
StandardCreator<MyProduct> myCreator;
- מוסכמות בקשר לשמות - טוב להתרגל להשתמש במוסכמות בקשר לשמות שיבהירו את העובדה שמשתמשים ב factory method. למשל, ניתן להכריז על הפעולה האבסטרקטית שמגדירה את ה factory method כ
Class* DoMakeClass()
כאשר כאן Class הוא מחלקת ה product.
קוד לדוגמה:
נשתמש בדוגמה של המבוך אשר מתוארת כרקע ל abstract factory pattern. אם לא קראתם אותה עדיין, או אם סתם שכחתם אותה
נא קראו אותה שוב. כזכור, הפונקציה
CreateMaze() בונה ומחזירה מבוך. בעיה אחת איתה היא שהיא מפרטת בקוד את המחלקות שהיא יוצרת
(Maze, Room, Door, Wall). אנו נציג factory methods אשר מאפשרות ל subclass ים לקבוע אילו סוגי מרכיבים יווצרו.
תחילה נגדיר factory methods ב MazeGame כדי ליצור את אובייקטי ה maze, ה room, ה door וה wall:
class MazeGame {
public:
Maze* CreateMaze();
// factory methods:
virtual Maze* MakeMaze() const
{ return new Maze; }
virtual Room* MakeRoom(int n) const
{ return new Room(n); }
virtual Wall* MakeWall() const
{ return new Wall; }
virtual Door* MakeDoor (Room* r1, Room* r2) const
{ return new Door(r1, r2); }
};
כל factory method מחזירה מרכיב של מבוך מ type מסוים. MazeGame
מספק מימושי ברירת מחדל שמחזירים את הסוגים הפשוטים ביותר של מבוך, חדר, קיר ודלת. כעת אפשר לכתוב מחדש את
CreateMaze() כך שתשתמש ב factory methods הללו:
Maze* MazeGame::CreateMaze() {
Maze* aMaze = MakeMaze();
Room* r1 = MakeRoom(1);
Room* r2 = MakeRoom(2);
Door* theDoor = MakeDoor(r1, r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, MakeWall());
r1->SetSide(East, theDoor);
r1->SetSide(South, MakeWall());
r1->SetSide(West, MakeWall());
r2->SetSide(North, MakeWall());
r2->SetSide(East, MakeWall());
r2->SetSide(South, MakeWall());
r2->SetSide(West, theDoor);
return aMaze;
}
משחקים שונים יכולים לבצע subclassing ל MazeGame כדי לשנות חלקים מהמבוך. subclass ים של
MazeGame יכולים להגדיר מחדש חלק או את כל ה factory methods כדי לשנות את ה products הנוצרים. למשל
BombedMazeGame יכול להגדיר מחדש את Room ו
Wall כדי להחזיר את הווריאציות ה"מופצצות" שלהם:
class BombedMazeGame : public MazeGame {
public:
BombedMazeGame();
virtual Wall* MakeWall() const
{ return new BombedWall; }
virtual Room* MakeRoom(int n) const
{ return new RoomWithABomb(n); }
};
הווריאנט של EnchantedMazeGame יכול להיות מוגדר כך:
class EnchantedMazeGame : public MazeGame {
public:
EnchantedMazeGame();
virtual Room* MakeRoom(int n) const
{ return new EnchantedRoom(n, CastSpell()); }
virtual Door* MakeDoor (Room* r1, Room* r2) const
{ return new DoorNeedingSpell(r1, r2); }
protected:
Spell* CastSpell() const;
};
|
|
|
|