ראשי > abstract factory pattern > חלק שלישי
The Abstract Factory

קוד לדוגמה:
הרקע:
נשתמש בדוגמה נפוצה לבניית מבוך עבור משחק מחשב כדי להדגים את יישום ה abstract factory pattern. דוגמה זו תשמש לנו גם כרקע לקוד לדוגמה ב
factory method pattern. לא נרד לפרטים מעמיקים של מה יכול להיות בתוך מבוך אלא נתמקד בכיצד מבוכים נוצרים. נגדיר מבוך כקבוצת חדרים. חדר מכיר את שכניו כאשר שכנים אפשריים הם חדר, קיר או דלת לחדר אחר. המחלקות Room, Door, Wall יגדירו את מרכיבי המבוך בדוגמה. גם כאן נתבונן רק בפרטים שמעניינים אותנו בתוך מרכיבים אלה. לחץ על הציור כדי לצפות בדיאגרמת היחסים בין מרכיבי המבוך. אם אינכם זוכרים את הסימונים אפשר להתרענן .

לחצו להגדלה
לחץ להגדלה - maze relationships

לכל חדר 4 צדדים שנממש ע"י

enum Direction {North, South, East, West};

המחלקה MapSite היא המחלקה האבסטרקטית עבור מרכיב במבוך. לשם הפשטות היא מממשת רק פעולה אחת, Enter(), שמפרטת מה קורה כאשר מנסים להכנס דרך אחד הצדדים. פעולה זו תהווה בסיס לפעולות יותר מורכבות:

Class MapSite {
Public:
    virtual void Enter() = 0;
};

המחלקה Room היא concrete subclass של MapSite ומגדירה את יחסי המפתח בין מרכיבי המבוך:


class Room : public MapSite {
public:
     Room (int roomNo);

     MapSite* GetSide (Direction) const;
     Void SetSide(Direction, MapSite*):
private:
     MapSite* _sides[4];
     int _roomNumber;
};

המחלקות הבאות מייצגות קיר או דלת שיכולים להיות בצדי החדר:


class Wall : public MapSite {
public:
     Wall();

virtual void Enter();
};

class Door : public MapSite {
public:
     Door(Room* = 0, Room* = 0);

     virtual void Enter();
     Room* OtherSideFrom(Room*);

private:
     Room* _room1;
     Room* _room2;
     bool _isOpen;
};

ולבסוף נגדיר את מחלקת המבוך עצמה שמחזיקה את אוסף החדרים ויודעת למצוא חדר בהינתן מספרו:


class Maze {
public:
     Maze();

     Void AddRoom(Room*);
     Room* RoomNo(int) const;
private:
     //…
};

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


Maze* MazeGame::CreatMaze () {
     Maze* aMaze = new Maze;
     Room* r1 = new Room(1);
     Room* r2 = new Room(2);
     Door* theDoor = new Door(r1, r2);

     aMaze->AddRoom(r1);
     aMaze->AddRoom(r2);

     r1->SetSide(North, new Wall);
     r1->SetSide(East, theDoor);
     r1->SetSide(South, new Wall);
     r1->SetSide(Wast, new Wall);

     r2->SetSide(North, new Wall);
     r2->SetSide(East, new Wall);
     r2->SetSide(South, new Wall);
     r2->SetSide(West, theDoor);

     return aMaze;
}

הפונקציה היא די מסובכת בהתחשב במה שהיא עושה, יצירת מבוך עם שני חדרים. יש דרכים לפשט אותה אך הבעיה העיקרית שלה היא inflexibility. היא מפרטת בקוד את מבנה המבוך. כשנרצה לשנות את מבנה המבוך משמעות הדבר לשנות את הפונקציה CreateMaze(). יהיו שתי דרכים, לא ברור מי יותר גרועה: או לשנות את כל הפונקציה ע"י דריסה שלה, כלומר לממש את כולה מחדש , או לשנות חלקים שלה, דבר שמועד לפורענות מראש ואינו מאפשר reuse. עד כאן הרקע. אם קראתם רקע זה כי "קפצתם" לכאן מה factory method pattern פשוט לחצו כאן כדי לחזור.

למעלה

הקוד:
ה abstract factory pattern מראה איך לפתור את בעיית ה flexibility ביצירת המרכיבים של המבוך כאשר הקוד לא בהכרח יהיה יותר קטן. שימוש בה יקל עלינו לשנות מרכיבים שונים של המבוך. ולכן נראה איך שימוש בה בפונקציה CreateMaze() מפשט ומאפשר ליצור בקלות סוגים חדשים של מבוך.
נניח שנרצה להשתמש בקוד הקיים עבור משחק חדש עם מבוך מכושף... יהיו לו מרכיבים נוספים כמו דלת שדורשת כישוף וכו'. איך נשנה את CreateMaze() בקלות כך שתיצור מבוך מהסוג החדש?
פשוט מאוד: ניישם את מה שלמדנו עד עתה בשקידה רבה ונשתמש ב...נכון! ב abstract factory pattern!
על ידי העברה של Factory כפרמטר ל CreateMaze() נוכל לקבוע את סוג האובייקטים שיווצרו. ניצור מחלקה MazeFactory שתייצר מרכיבים של מבוך. היא תבנה דלתות, חדרים וקירות ביניהם. כל תוכנית שתרצה לבנות מבוך תקח MazeFactory כפרמטר, וע"י קביעת סוג ה MazeFactory שהיא תקבל (MazeFactory עצמו הוא מחלקה אבסטרקטית) יקבע הסוג של המבוך שיווצר על כל מרכיביו. כלומר: צריך לבחור בנקודה אחת בקוד איזה סוג של מבוך רוצים וה Factory כבר ידאג לכל השאר, כמו שאנחנו אוהבים...
אם כן נראה איך זה מתבטא בקוד:


class MazeFactory {
public:
     MazeFactory();

     virtual Maze* MakeMaze() const
         { return new Maze; }
     virtual Wall* MakeWall() const
         { return new Wall; }
     virtual Room* MakeRoom (int n) const
         { return new Room (n); }
     virtual Door* MakeDoor (Room* r1, Room* r2) const
         { return new Door (r1, r2); }
};

(כאן MazeFactory אינו מחלקה אבסטרקטית ולכן יכול לשמש כ Factory בעצמו)
כעת נראה גרסה של CreateMaze() שמנצלת את השימוש ב MazeFactory אשר מגיעה אליה כפרמטר (השינויים מסומנים בצבע שונה) :



Maze* MazeGame::CreateMaze (MazeFactory& factory) {
     Maze* aMaze = factory.MakeMaze();
     Room* r1 = factory.MakeRoom(1);
     Room* r2 = factory.MakeRoom(2);
     Door* aDoor = factory.MakeDoor(r1, r2);

     aMaze->AddRoom(r1);
     aMaze->AddRoom(r2);

     r1->SetSide(North, factory.MakeWall());
     r1->SetSide(East, aDoor);
     r1->SetSide(South, factory.MakeWall());
     r1->SetSide(Wast, factory.MakeWall());

     r2->SetSide(North, factory.MakeWall());
     r2->SetSide(East, factory.MakeWall());
     r2->SetSide(South, factory.MakeWall());
     r2->SetSide(West, aDoor);

     return aMaze;
}

כעת, נניח שמאוד ישעמם לנו ופתאום נרצה ליצור מבוך מכושף. נגדיר subclass ל MazeFactory, שייקרא EnchantedMazeFactory, ושידרוס את הפעולות הרלוונטיות ב MazeFactory ויחזיר subclasses שונים ל Room, Door וכו'.


class EnchantedMazeFactory : public MazeFactory {
public:
     EnchantedMazeFactory();

     virtual Room* MakeRoom(int n) const
         { return new EnchantedRoom(n, CastSpell()); }

     virtual Door* MakeDoor (Room* r1, Room* r2) const
         { return new DoorNeedingSpell(r1, 42); }

protected:
     Spell* CastSpell() const;
};

(DoorNeedingSpell ו EnchantedRoom הם כמובן subclass של Room, Door בהתאמה)

כעת, נניח שישעמם לנו עוד יותר: נניח שנרצה לאפשר סוג של חדר שיש בתוכו פצצה. אם היא מתפוצצת היא גורמת לנזק לקירות. אפשר ליצור subclass של Room שיוכל לבדוק אם יש פצצה בחדר ואם היא התפוצצה, ו subclass של Wall שיוכל לבדוק אם הוא נפגע כתוצאה מפיצוץ. כמו כן נבנה BombedMazeFactory שהוא subclass של MazeFactory שיבטיח לנו שהקירות והחדרים יהיו מהסוג המתאים. נדרוס שתי פעולות:


Wall* BombdMazeFactory::MakeWall () const {
     Return new BombedWall;
}

Room* BombedMazeFactory::MakeRoom (int n) const {
     Return new RoomWithABomb (n);
}

כעת כדי לבנות מבוך עם פצצות בסה"כ נקרא ל CreateMaze() עם BombedMazeFactory כפרמטר:


MazeGame game; //this code always the same
BombedMazeFactory factory;
game.CreateMaze(factory); //this code always the same

הקוד באדום זוהי הנקודה היחידה (!) בתוכנית בה אנו קובעים עם אילו אובייקטים נעבוד. אם נרצה לבנות מבוך מכושף פשוט נקרא ל CreateMaze() עם EnchantedMazeFactory כפרמטר, וכן הלאה מבוכים ממבוכים שונים כיד הדמיון הטובה עליכם...
הערה: יתכן ובחלק מהמבוכים השונים יש פעולות שספציפיות לאותו מבוך ולכן לא ניתן לגשת אליהם מהממשק המשותף, לדוגמה: בחדר עם פצצה יש פעולה ספציפית לו לבדוק האם היא התפוצצה. פעולה זו אינה קיימת בממשק של המחלקה חדר. כדי לגשת לפעולה זו יש צורך ב dynamic_cast וכאן מתגלה יתרון נוסף של ה abstract factory pattern :
dynamic_cast עובד רק אם ה type של האובייקט הוא באמת זה שמצפים לו, ו abstract factory pattern אכן מבטיחה שכל המוצרים המיוצרים בתוכנית הם מאותו סוג מכיוון שיש נקודה אחת בקוד שבה מושפע הסוג שלהם וזו נקודת בחירת הFactory . ה Factory שנבחר יכול לייצר מוצרים מסוג אחד בדיוק ולכן לא יכול לקרות מצב שלפתע נתקלים בסוג שלא ציפינו לו ( מגניב !!!).

אני מקווה ש"קניתם" את ה abstract factory pattern כי זה אחד המוצרים היותר טובים בשוק.

למעלה







 
מה בעמוד:
 
קוד לדוגמה -
הרקע
הקוד