ינו' 05 2010

Multiple Singletons

מאת נחשון בתאריך 11:41 נושאים נושאים מתקדמים

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

תזכורת: Singleton pattern

Singleton pattern, הוא אחד הpatterns הפופולאריים, והקלים ליישום ולהבנה. הוא נועד כדי ליצור מחלקות מהן ניתן ליצור אובייקט אחד בלבד בתוכנה. משתמשים בו לייצוג משאבי מערכת כגון שעון המערכת, בשמירת תצורת התוכנה (configuration), logger ועוד.

ביישום אנחנו נרצה לאפשר למחלקה, ולה בלבד, לייצר אובייקטים מסוגה ע"י הפיכת הconstructors שלה לפרטיים. ניצור משתנה סטטי פרטי במחלקה (משתנה ששייך למחלקה ולא לאובייקט ספציפי) מסוג המחלקה או מסוג מצביע למחלקה. כמו כן, ניצור פונקציה סטטית בשם Instance שמחזירה reference או מצביע לאובייקט הסטטי.

class Logger
{
public:
    static Logger *Instance(){
        if ( instance_ == NULL ) {
            instance_ = new Logger();
        }
        return instance_;
    }

    bool FuncA();
    void FuncB(char *var);

protected:
    static Logger *instance_;
    Logger(){};
    Logger(Logger &log); // prevent copy by make it private
    Logger &operator=(Logger &log); // prevent assignment by making = operator private
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

if ( Logger::Instance->FuncB() ) {
    do_something();
}
1
2
3

Singletons מרובים? זו לא סתירה?

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

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

פתרונות אפשריים

הטריוויאלי

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

class Logger
{
public:
    static Logger *Log(){
        if ( myLog_ == NULL ) {
            myLog_ = new Logger();
        }
        return myLog_;
    }

        static Logger *Trace(){
                if ( myTrace_ == NULL ) {
                        myTrace_ = new Logger();
                }
                return myTrace_;
        }

    bool Init();
    void AddLine(char *var);

protected:
    static Logger *myLog;
    static Logger *myTrace;
    Logger(){};
    Logger(Logger &log); // prevent copy by make it private
    Logger &operator=(Logger &log); // prevent assignment by making = operator private
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

השימוש יהיה גם הוא טריוויאלי:

Logger::Log->AddLine(msg);
if ( TraceEnabled ) {
        Logger::Trace->AddLine("message was sent to IP:123.78.10.45");
}
1
2
3
4

הקצת פחות טריוויאלי

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

class Logger
{
public:
   
    typedef enum { LOG, TRACE, NUM_OF_LOGGERS } LogType;
   
    static Logger *Instance(LogType i)
    {
        if ( i < NUM_OF_LOGGERS ) {
            if ( Loggers[i] == NULL ) {
                Loggers[i] = new Logger();
            }
            return Loggers[i];
        }
        throw string("out of array boundaries");
    }

    bool Init();
    void AddLine(char *var);

protected:
    static Logger *Loggers[NUM_OF_LOGGERS];
    Logger(){};
    Logger(Logger &log); // prevent copy by make it private
    Logger &operator=(Logger &log); // prevent assignment by making = operator private
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

וכך נשתמש במחלקה logger:

Logger::Instances(Logger::LOG)->AddLine("My logger line");
Logger::Instances(Logger::TRACE)->AddLine("My trace line");
1
2

הלא כל כך טריוויאלי

הפתרון הזה הוא קצת כבד ובדרך כלל מיותר וסובל מעודף תכנון (over engineering). בכל זאת, ייתכן כי במקרים מסויימים הוא בדיוק זה שיתאים. נניח ששני הלוגרים שלנו, הlog והtrace אינם זהים. ישנו פרט או שניים בתפקוד שלהם שמבדיל בניהם. נוכל כמובן להתנות את ההתנהגות ע"י משפט if בתוך קוד המחלקה, אבל אז המחלקה אולי "מודעת" יותר מדי לאובייקטים שלה, מה שבדרך כלל לא מספק עצמאות מספיק טובה של המחלקה.

Logger יהיה עכשיו מחלקה ממנה נירש את הsingletons שלנו. מכיוון שהconstructos שלה מוגנים (הם protected) לא נוכל ליצור ממנה אובייקטים. נכתוב במחלקת הבסיס את כל הפונקציונליות המשותפת לשני הלוגרים שלנו, ורק את ההבדלים בניהם נכתוב במחלקות היורשות. לצורך העניין, ננסה להקל על המתכנת השישתמש בלוגרים שלנו כך שהוא לא יצטרך בכל פעם לשאול האם הtrace מאופשר או לא. הtrace יקרא מתוך הקונפיגורציה של התוכנה האם יש צורך לכתוב או לא. המשתמש פשוט יכתוב לtrace בכל פעם שהוא ירצה. נשנה את הפונקציה Init כך שתקרא מהקונפיגורציה ערך בוליאני האם לכתוב לtrace או לא. לפני הכתיבה בפועל, נשאל האם יש בה צורך. הלוג יירש את כל תכונות הבסיס ללא שינוי.

להלן מחלקת הבסיס Logger:

class Logger
{
public:
    virtual bool Init();
    virtual void AddLine(string line);

protected:
    Logger(){};
    Logger(Logger &log); // prevent copy by make it private
    Logger &operator=(Logger &log); // prevent assignment by making = operator private
};
1
2
3
4
5
6
7
8
9
10
11

המחלקה Log יורשת מLogger ומוסיפה את המימוש של הsingleton: פונקציה סטטית בשם Instance, ומשתנה סטטי אחד בשם myLog_ שהוא מצביע לסוג לLog.

class Log : public Logger
{
public:
    static Log *Instance()
    {
        if ( myLog_ == NULL ) {
            myLog_ = new Log();
        }
        return myLog_;
    }

private:
    static Log *myLog_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

המחלקה Trace יורשת אף היא מLogger וגם היא מוסיפה את המימוש לsingleton. בנוסף, היא מממשת את הפונקציות הווירטואליות Init ו AddLine. בגרסה שלה לInit, המחלקה קוראת ערך בוליאני מתוך הקונפיגורציה של התוכנה, שקובע אם ליצור trace או לא. המחלקה שומרת את הערך אצלה במשתנה פרטי בשם doTrace_. רק אם התוכנה מקונפגת ליצור trace, הפונקציה ממשיכה וקוראת לפונקציה של מחלקת הבסיס Logger. בפונקציה AddLine, אנחנו בודקים קודם כל את doTrace_ ורק אז ממשיכים לגרסת מחלקת הבסיס.

class Trace : public Logger
{
public:
    static Trace *Instance()
    {
        if ( myTrace_ == NULL ) {
            myTrace_ = new Trace();
        }
        return myTrace_;
    }
   
    virtual bool Init()
    {
        // reading boolean configuration, is trace configurated in the system?
        doTrace_ = SystemConfiguration::Instance->DoTrace();
       
        if ( doTrace_ ) { // trace is configurated in the system
            // call base version of the function
            return Logger::Init();
        }
        return false;
    }
   
    virtual void AddLine(string line)
    {
        if ( doTrace_ ) {
            Logger::AddLine(line);
        }
    }

private:
    static Trace *myTrace_;
    bool doTrace_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

השימוש במחלקות שלנו ייראה פחות או יותר כך:

Log::Instance()->Init();
Trace::Instance()->Init();
   
Log::Instance()->AddLine("Testing Log");
Trace::Instance()->AddLine("Testing Trace");
1
2
3
4
5

כאשר השורה "Testing Trace" תודפס אך ורק אם התוכנה רצה עם קונפיגורציה שבה trace מאופשר.

אין תגובות

כתובת טרקבק | RSS תגובות

השארת תגובה

Get Adobe Flash playerPlugin by wpburn.com wordpress themes