|
 |
|
כמה נקודות בקשר למימוש:
במימוש של ה Composite pattern ישנן נקודות רבות שיש להתייחס אליהן:
- reference מפורש להורה - אם נחזיק אצל ילדים הצבעה מפורשת להורה זה יכול לפשט ולהקל את הטיפול במבנה ה composite. זה מאפשר, למשל, טיול במעלה המבנה ומחיקה של component. המקום הטבעי להגדיר reference להורה הוא בComponent class.
המחלקות Leaf ו Composite יירשו ממנו את ההצבעה, כמו גם את הפעולות הקשורות בה.
יש לשים לב שבמקרה זה יש לשמור ולתחזק את התכונה שתמיד ילד מצביע להורה האמיתי שלו שמחזיק אותו כילד. הדרך הפשוטה להבטיח זאת היא לדאוג שנשנה הורה של component כלשהו רק בעת שנוסיף או נוריד אותו מ composite כלשהו. אם זה יכול להיות ממומש פעם אחת בפעולות Add ו Remove של Composite, זה יעבור בירושה לכל ה subclass ים של Composite והתכונה תישמר באופן אוטומטי.
- ביצוע sharing ל Components - לעתים נרצה לבצע sharing ל Components לדוגמה כדי לחסוך במשאבים. כאשר ל component יכול להיות לא יותר מאשר הורה אחד זה בעייתי לעשות זאת. פתרון אפשרי הוא שלילד יוכלו להיות מספר הורים, אך זה יכול לגרום לכפילויות בבקשות שמתקדמות במעלה העץ.
- הרחבת הממשק של Component ככל שניתן - אחת המטרות ב composite pattern היא להפוך את הקליינט ללא מודע ל Leaf או ה Composite הספציפיים שהוא משתמש בהם. על מנת להשיג מטרה זו מחלקת ה Component צריכה להגדיר כמה שיותר פעולות שמשותפות ל Leaf ול Composite . בדרך כלל מחלקת ה Component תספק מימושי ברירת מחדל לפעולות אלה וה Leaf וה Composite ידרסו פעולות אלה כשנממש אותם.
בכל אופן, גישה זו קצת סותרת את העיקרון ש class יגדיר רק פעולות שיש להן משמעות אצל subclass ים שלו. ישנן פעולות רבות ש component תומך בהן והן לא נראות בעלות משמעות כלשהי עבור המחלקה Leaf אשר יורשת ממנו. למשל, אילו מימושי ברירת מחדל Component ייתן לפעולות אלה שאין להן משמעות אם נקרא להן מתוך Leaf?
לפעמים נדרשת מעט יצירתיות (מה לעשות...) על מנת להעביר פעולות שנראות כפעולות טהורות של Composite למחלקת ה Component. לדוגמה, הממשק לגישה לילד הוא כמובן פעולה טבעית של Composite בעוד שזו אינה פעולה הכי טבעית עבור Leaf... אבל עם מעט דמיון נוכל להסתכל על Leaf כמחלקה שיש לה ילדים, ומספרם אפס.
כך נוכל לממש את פעולת הגישה לילדים ב Component ולתת לה מימוש ברירת מחדל שלא מחזיר אף ילד. כך נוכל להשתמש בה בLeaf ולדרוס אותה על ידי מימוש מתאים ב Composite.
הפעולות לטיפול בילדים הם מעט בעייתיות ונדסקס בהם יותר בסעיף הבא.
- הכרזות על פעולות טיפול בילדים -למרות שמחלקת ה Composite מממשת את פעולות Add() ו Remove() עבור טיפול בילדים, נושא חשוב ב Composite pattern הוא באיזה class נכריז
על פעולות אלה בהיררכית המחלקות של ה Composite pattern . האם להכריז עליהן ב Component ולתת להם מימוש שיתאים גם ל Leaf (שיירש אותם), או שמא נכריז עליהן רק ב Composite ונממש אותן רק בתתי המחלקות שלו?
זה תלוי מה חשוב לנו יותר: שקיפות ל client או בטיחות.
אם נגדיר את הממשק בראש ההיררכיה, כלומר ב Component, זה מגביר את השקיפות עבור ה client. הוא אינו חייב לדעת עם איזה סוג הוא מתקשר כרגע, אלא הוא יכול להתייחס באופן שווה גם ל Composite וגם ל Leaf . כמובן, במקרה זה יתכן ו client ינסה לבצע פעולות חסרות משמעות כמו הוספת ילד ל Leaf .
אם נגדיר את הממשק ב Composite נקבל בטיחות יותר גבוהה מכיוון שכל נסיון להוסיף או להוריד ילד ל Leaf יתגלה בזמן קומפילציה (בשפות שמקומפלות). התשלום הוא פחות שקיפות כי ל Leaf ול Composite יהיו כעת ממשקים קצת שונים.
בתיאור שלנו של ה Comopsite pattern הודגשה השקיפות על פני הבטיחות והגדרנו את הממשק לטיפול בילדים בראש ההיררכיה, ב Component.
במקרים שנעדיף בטיחות נגדיר את הממשק רק ב Composite אך הבעיה כעת היא שנצטרך להשתמש לעתים קרובות ב type-checking על מנת לברר את סוג האובייקט לפני ביצוע פעולות מסוימות, כדי לבצע את הפעולה הנכונה בהתאם לסוג האובייקט. הדרך היחידה לספק שקיפות היא להגדיר את
Add() ו Remove() ב Compnent . אבל זה יוצר בעיה חדשה: אם ננסה לתת להם מימוש ברירת מחדל (יפעל במקרה שזה Leaf)
שלא מבצע כלום זו תהיה התעלמות מהיבט חשוב; נסיון להוסיף או להוריד ילד ל Leaf קרוב לוודאי מצביע על באג. במקרה זה פעולת ה
Add() מייצרת garbage (האובייקט שאותו רוצים להוסיף). אפשר לממש את פעולת ה Add() כך שתמחק אותו אך זה לא מה שהclient מצפה לו. בדרך כלל עדיף שמימוש ברירת המחדל שלהן יהיה להכשל, וכך
Add() ו Remove() יכשלו אוטומטית אם הן יקראו במקרים לא מתאימים.
- האם Component צריך לממש רשימה של Component-ים? - יתכן ותתפתו להגדיר את המצביע לקבוצת הילדים ב Component, היכן שממומשות פעולות Add() ו Remove(). זיכרו רק, שעל ידי כך תגרמו לבזבוז מקום גם בLeaf ים ללא צורך. אפשר לעשות זאת עבור מבנים קטנים שבהם בזבוז המקום לא יהיה משמעותי.
- סדר בילדים - בהרבה מקרים יהיה חשוב לנו הסדר בו מסודרים הילדים. למשל, בדוגמה הגרפית שהוזכרה מקודם, יתכן שמשמעות הסדר של הילדים תהיה העדיפות שלהם מלפנים לאחור בתמונה. אם Composite ייצג עץ גזירה, Compound statements יהיו מופעים של Composite אשר ילדיהם חייבים להיות מסודרים כדי לשקף נכונה את התוכנית.
כאשר צריך לקחת בחשבון את סדר הילדים, צריך לתכנן בזהירות ובקפדנות את פעולות הגישה לילדים והתפעול שלהם.
- Caching על מנת לשפר ביצועים -- אם במבנה Composite מסוים נצטרך לטייל או לחפש ילדים לעתים קרובות, מחלקת ה Composite יכולה לשמור ב cache מידע מינימלי על טיולים או על ילדים שיעזור לנו ויקצר לנו את התהליך. למשל, המחלקה Picture מהדוגמה במוטיבציה יכולה לשמור ב cache את ה bounding box של ילדיה. בזמן ציור, או בחירה, מידע זה יאפשר ל Picture להימנע מלצייר את ילדיו או לחפש אותם בזמן שהם אינם גלויים לעין בחלון הנוכחי.
שינויים ב Component ידרשו ביצוע שינויים ב cache של ההורה שלו. לכן זה יעבוד טוב כאשר Component ים יכירו את הוריהם.
- מי מוחק Components? - בשפות ללא garbage collection בדרך כלל עדיף לתת ל Composite את האחריות עבור מחיקת הילדים, ולתת לו שיבצע זאת תוך כדי שהוא נמחק (ב destructor). זאת פרט למקרים שבהם ניתן לבצע sharing ל Leaf ים.
- באיזה מבנה נתונים נאחסן Components? - ה Composites יכולים להשתמש במגוון של מבני נתונים לאחסון של הילדים, כמו רשימות מקושרות, מערכים, עצים ו hash tables. הבחירה במבנה הנתונים תלויה כמובן, כתמיד, ביעילות. למעשה, זה אפילו אינו נחוץ לשמור את הילדים במבנה נתונים כללי כלשהו. ישנם מקרים שבהם נשמור משתנה לכל ילד למרות שבמקרה זה כל subclass יצטרך לממש ממשק משלו לטיפול בילדים.
לאחר התפלפלויות ארוכות אלה בסוגיית המימוש והיבטיה השונים, נראה מעט קוד לדוגמה.
קוד לדוגמה:
ציוד אלקטרוני כדוגמת מחשב אישי או מערכת סטריאו לעתים קרובות מאורגן כהיררכיות רקורסיביות של מרכיבים.
למשל, מארז יכול להכיל תושבת, bus ועוד. תושבת של מחשב יכולה להכיל כוננים ולוחות, bus יכול להכיל כרטיסים, וכן הלאה. מבנים כאלה יכולים להיות ממודלים באופן טבעי בעזרת ה Composite pattern. נגדיר מחלקה כללית,
Equipment, אשר תגדיר ממשק כללי עבור מרכיב כלשהו בהיררכיה:
class Equipment {
public:
virtual ~Equipment();
const char* Name() { return _name;}
virtual Watt Power();
virtual Currency NetPrice();
virtual Currency DiscountPrice();
virtual void Add (Equipment*);
virtual void Remove (Equipment*);
virtual Iterator<Equipment*>* CreateIterator();
protected:
Equipment (const char*);
private:
Const char* _name;
};
Equipment מגדיר פעולות שמחזירות תכונות של חלק מסוים, כמו למשל צריכת כוח ומחיר. תת המחלקות של Equipment מממשות פעולות אלה בהתאם.
Equipment גם מגדירה את הפעולה CreateIterator() שמחזירה Iterator עבור גישה לחלקיה. מימוש ברירת המחדל של פעולה זו הוא NullIterator אשר מבצע איטרציה על קבוצה ריקה. Subclass ים של
Equipment יכולים להיות מסוג Leaf כמו כונני דיסקים, מעגלים משולבים ו switch ים:
class FloppyDisk : public Equipment {
public:
FloppyDisk (const char*);
virtual ~FloppyDisk();
virtual Watt Power();
virtual Currency NetPrice();
virtual Currency DiscountPrice();
};
CompositeEquipment הוא מחלקת הבסיס עבור Equipment שמכיל Equipment ים אחרים. גם הוא subclass של Equipment:
class CompositeEquipment : public Equipment {
public:
virtual ~CompositeEquipment();
virtual Watt Power();
virtual Currency NetPrice();
virtual Currency DiscountPrice();
virtual void Add(Equipment*);
virtual void Remove (Equipment*);
virtual Iterator<Equipment*>* CreateIterator();
protected:
CompositeEquipment(const char*);
private:
List<Equipment*> _equipment;
};
CompositeEquipment מגדיר את הפעולות עבור גישה וטיפול ב subequipment. הפעולות Add() ו Remove()
מכניסות ומוחקות חלק מרשימת ה Equipment ים שנקראת כאן _equipment . הפעולה CreateIterator() מחזירה Iterator
(מופע של ListIterator) אשר יכול לטייל על הרשימה. מימוש ברירת מחדל של NetPrice() יכול להשתמש ב CreateIterator()
על מנת לסכום את ה NetPrice() ים של הילדים, שהם Equipment ים בעצמם.
Currency CompositeEquipment::NetPrice() {
Iterator<Equipment*>* i = CreateIterator();
Currency total = 0;
For(i->First(); !i->IsDone(); i->Next()) {
Total += i->CurrentItem()->NetPrice();
}
delete i;
return total;
}
כעת אפשר לתאר תושבת במחשב כ subclass של CompositeEquipment שנקרא Chassis. הוא יירש את הפעולות שקשורות לילדים מ
CompositeEquipment:
class Chassis : public CompositeEquipment {
public:
Chassis(const char*);
virtual ~Chassis;
virtual Watt Power();
virtual Currency NetPrice();
virtual Currency DiscountPrice();
};
ואפשר להגדיר קונטיינרים אחרים ל Equipment , כמו Cabinet ו Bus , באופן דומה. זה נותן לנו כל מה שצריך כדי למדל את הציוד שיש במחשב (במחשב פשוט מאוד):
Cabinet* cabinet = new Cabinet ("PC Cabinet");
Chassis* chassis = new Chassis ("PC Chassis");
Cabinet->Add(chassis);
Bus* bus = new Bus("MCA Bus");
bus->Add(new Card("16Mbs Token Ting"));
Chassis->Add(bus);
Chassis->Add(new FloppyDisk("3.5in Floppy"));
Cout << "The net price is " << chassis->NetPrice() << endl;
|
|
 |
|