Σάββατο, 13 Αυγούστου 2011

Χειρισμός γεγονότων χρησιμοποιώντας τη μέθοδο του παρατηρητή (C++)

Πολλές φορές σε ένα πρόγραμμα, μπορεί να θέλουμε μερικές οντότητες να συμπεριφέρονται με βάση τη συμπεριφορά (το τί συμβαίνει) κάποιας άλλης οντότητας. Παίρνοντας για παράδειγμα το γνωστό cartoon, Tom & Jerry, ο Tom κυνηγάει τον Jerry μόλις εκείνος βγει από τη φωλιά του!
Ο Tom και ο Jerry είναι οι δύο οντότητες. Το ότι ο Jerry εξέρχεται από τη φωλιά του είναι το γεγονός.

Σε κάθε τέτοια κατάσταση όπως παραπάνω ένα ον παίζει το ρόλο του υποκειμένου και ένα ή παραπάνω όντα παίζουν το ρόλο του παρατηρητή. Εύκολα καταλαβαίνουμε πως στο παράδειγμά μας το υποκείμενο είναι ο Jerry και παρατηρητής είναι ο Tom! Φυσικά, αλλάζοντας λίγο (ως πολύ!) το γνωστό cartoon, θα μπορούσαμε να έχουμε πολλούς Tom (παρατηρητές) να "παρατηρούν" τις κινήσεις του Jerry. Ας δούμε τα παραπάνω και λίγο προγραμματιστικά...


Από προγραμματιστικής πλευράς (σε C++), όλες οι οντότητες (είτε αυτές είναι υποκείμενα, είτε παρατηρητές) είναι κλάσεις (class). Ο Tom είναι μια κλάση και ο Jerry μια άλλη κλάση, που σημαίνει ότι ο καθένας έχει τις δικές του ιδιότητες. Αν και θα ακουστεί λίγο... "φιλοσοφία", ο Tom είναι Tom, αλλά είναι και παρατηρητής (is an observer)! Αμέσως αμέσως μπαίνει στο παιχνίδι η κληρονομικότητα και κατ' επέκταση η αναγκαιότητα της αντικειμενοστρέφειας (εξ' ού και C++). Ο Jerry είναι απλά ο Jerry, αλλά επειδή είναι το υποκείμενο στο παράδειγμά μας, οι κινήσεις του (γεγονότα) λειτουργούν σαν "διακόπτης" για τις αντιδράσεις του Tom! Όταν ο Jerry βγαίνει από τη φωλιά του, αυτό είναι εμφανές (ο Jerry δεν είναι αόρατος!) και δίνει αφορμή στον Tom να ξεκινήσει το κυνήγι! Άρα στην κλάση του Jerry πρέπει να υπάρχουν συναρτήσεις "triggers" που ενημερώνουν τον έξω κόσμο σχετικά με τη συμπεριφορά του. Αυτό στην πραγματικότητα είναι αυτονόητο, αλλά προγραμματιστικά αυτές οι συναρτήσεις είναι απολύτως αναγκαίες.
Εκτός από αυτό ο Jerry ξέρει ποιοί είναι οι εχθροί-παρατηρητές του (ο Tom) και κατ' επέκταση γνωρίζει ποιοί θα τον κυνηγήσουν όταν βγει από τη φωλιά του. Άρα τα σήματα από τις κινήσεις του πρέπει να αποστέλλονται σε όλους τους παρατηρητές. Επομένως ο Jerry περιλαμβάνει στα μέλη της κλάσης του και μια δομή που να περιλαμβάνει τους παρατηρητές του.
Μετά από όλα αυτά ορίζουμε τις class observer και class jerry ως εξής:

class observer {
public:
    virtual void notify () = 0;
};

class jerry {
private:
    list<observer*> observers;
    bool in;
public:
    jerry ();
    void get_out ();
    void registerObserver (observer*);
    void unregisterObserver (observer*);
    void notifyObservers ();
};


Βλέπουμε πως η κλάση observer δεν έχει private κομμάτι, παρά μόνο μια public συνάρτηση, η οποία μάλιστα είναι pure virtual, που σημαίνει πως δεν έχει ορισμό, κάνει την κλάση αφηρημένη (abstract class) και δεν καλείται ουσιαστικά ποτέ.
Το ότι η class observer είναι abstract σημαίνει πως απαγορεύεται (και δεν είναι δυνατό) να δημιουργηθούν στιγμιότυπά της.
Η class jerry έχει στο private κομμάτι της μια δομή (STL List) με δείκτες σε observer (παρατηρητές) και μια λογική μεταβλητή in που κρατάει την κατάσταση του πού βρίσκεται ο Jerry (μέσα στη φωλιά ή έξω).
Στο public κομμάτι της (αλληλεπίδραση με τον έξω κόσμο) περιέχει αρχικά έναν constructor:

jerry::jerry () {
    cout << "Jerry is in his little 'house'..." << endl;
    in = true;
}


Αρχικά ο Jerry "γεννιέται" μέσα στη φωλιά του. Τίποτα παράξενο!
Επίσης περιέχει μια συνάρτηση get_out(). Στην ουσία αυτό που κάνει η get_out είναι να "βγάζει" τον Jerry εκτός φωλιάς και να ενημερώνει τους παρατηρητές του καλώντας την notifyObservers() που βρίσκεται επίσης στο public κομμάτι του:

void jerry::get_out () {
    in = false;
    cout << "Jerry has just got out! What is Tom gonna do?" << endl;
    notifyObservers();
}


Είναι λοιπόν ευθύνη της notifyObservers να ενημερώσει ΟΛΟΥΣ τους παρατηρητές του Jerry για το τί έγινε! Και το κάνει με τον ακόλουθο τρόπο:

void jerry::notifyObservers () {
    list<observer*>::iterator it;
    for (it = observers.begin(); it != observers.end(); ++it)
        (*it)->notify ();
}


Ορίζουμε έναν iterator για να προσπελαύνουμε τη λίστα δεικτών σε observer και για κάθε observer καλούμε την αντίστοιχη notify. Και εδώ είναι το μεγάλο ερώτημα: ΠΟΙΑ notify θα κληθεί αφού η κλάση observer είναι abstract και η notify της pure virtual; Όπως σωστά πρέπει να έχετε μαντέψει, η observer είναι η base class από την οποία κληρονομούν οι "πραγματικοί" observers του Jerry! Η class tom κληρονομεί από την observer, γιατί ο Tom είναι ένας observer. Αν είχαμε και ένα φίδι να κυνηγάει-παρατηρεί τον Jerry το φίδι θα κληρονομούσε επίσης από την observer! Βλέπουμε πως είναι δυνατό με αυτό τον τρόπο να έχουμε πολλούς παρατηρητές και μάλιστα (το πιο σημαντικό) διαφορετικού τύπου. Εδώ είναι η μαγεία της κληρονομικότητας και της λειτουργίας του observer pattern.

class tom : public observer {
public:
    void notify ();
    void startHaunt ();
};


Στην πιο απλή της μορφή η κλάση Tom δεν έχει private κομμάτι (δεν χρειαζόμαστε κάτι για το παράδειγμά μας). Επειδή ο Tom πρέπει να ειδοποιηθεί για τις αλλαγές στο υποκείμενο ΠΡΕΠΕΙ να έχει μια συνάρτηση notify στο public κομμάτι του, ώστε εκείνη να κάνει override την αντίστοιχη στην κλάση observer! Άρα στην κλήση της notify στην συνάρτηση notifyObservers() θα κληθεί η notify του Tom. Όπως καταλάβατε και το φίδι που λέγαμε προηγουμένως θα είχε και αυτό μια συνάρτηση notify στο public κομμάτι του και γενικά όποιος θέλει να λέγεται παρατηρητής πρέπει να έχει μια τέτοια συνάρτηση στο public κομμάτι του. Μόλις ο Tom λάβει το μήνυμα ότι ο Jerry βγήκε από τη φωλιά του (μα τον βλέπει! στην ουσία το μήνυμα αυτό είναι κι ας φαίνεται πιο περίπλοκο προγραμματιστικά!) είναι στη δική του μερίδα το πώς θα αντιδράσει. Στο παράδειγμά μας startHaunt()!

void tom::notify () {
    startHaunt ();
}

void tom::startHaunt () {
    cout << "Tom says: There you are you little scummy mouse!!"

         << endl;
}


Με το που δει ο Tom τον Jerry αρχίζει να τον κηνυγάει! Μπορεί εδώ να έχουμε απλά μια εκτύπωση που να μας δείχνει την αντίδραση, αλλά σκεφτείτε πόσο χρήσιμο θα ήταν να ξέρουμε τί έχει συμβεί σε ένα ον και ανάλογα να δράσουμε!
Στην class jerry βλέπουμε και δύο άλλες συναρτήσεις: την registerObserver και την unregisterObserver. Η πρώτη είναι απολύτως απαραίτητη για να "κατοχυρώσουμε" τον Tom ως παρατηρητή του Jerry στην αρχή του προγράμματός μας. Η δεύτερη είναι απαραίτητη μόνο αν θέλουμε κάποια στιγμή ένας παρατηρητής να μην παρακολουθεί πλεόν το υποκείμενο.
Στο παράδειγμά μας βέβαια, αυτό είναι λίγο δύσκολο! :P

void jerry::registerObserver (observer* obs) {
    observers.push_back(obs);
}

void jerry::unregisterObserver (observer* obs) {
    list<observer*>::iterator it;
    for (it = observers.begin(); it != observers.end(); ++it)
        if (*it == obs) it = observers.erase(it);
}


Και τελικά η main που θα εκμεταλλεύεται τις κλάσεις που έχουμε γράψει είναι η ακόλουθη:

int main () {
    jerry j;
    tom *t = new tom;
    j.registerObserver(t);
    j.get_out();
    delete t;
    return 0;
}


Αρχικά δημιουργείται ο Jerry και ύστερα ο Tom, τον οποίο Tom δημιουργούμε ΑΠΑΡΑΙΤΗΤΩΣ δυναμικά για να μπορέσει να δουλέψει το σχέδιό μας. Στη συνέχεια ο Tom κατοχυρώνεται ως παρατηρητής του Jerry καλώντας την registerObserver με παράμετρο τον δείκτη που δείχνει στον Tom. Ύστερα ο Jerry αποφασίζει να βγει από τη φωλιά του! Η έξοδος που δίνει το πρόγραμμα είναι η εξής:

Jerry is in his little 'house'...
Jerry has just got out! What is Tom gonna do?
Tom says: There you are you little scummy mouse!!


Το πηγαίο αρχείο μπορείτε να το κατεβάσετε από εδώ.

Τελειώνοντας, θέλω να ευχαριστήσω τον Νίκο Πρίντεζη (Solidus) για την πολύτιμη βοήθειά του!

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου