Design Patterns in C++; Singleton
Ελπίζω πως αυτή θα είναι η πρώτη δημοσίευση σε μία σειρά που θα ασχολείται με design patterns στη γλώσσα C++. Σε αυτό το πρώτο post λοιπόν θα προσπαθήσω να περιγράψω το Singleton Design Pattern.
Τί είναι Design Pattern
Design Pattern είναι μία τεκμηριωμένη λύση κάποιου σχεδιαστικού και αρχιτεκτονικού προβλήματος σε αντικειμενοστραφές προγραμματιστικό περιβάλλον. Ακόμα και αυτοί που δεν γνωρίζουν τον όρο είναι πολύ πιθανό να εφάρμοσαν ένα design pattern σε κάποιο πρόβλημα που αντιμετώπισαν.
Η τεκμηρίωση ενός Design Pattern ακολουθεί μία συγκεκριμένη δομή και αποτελείται από μια περιγραφή του προβλήματος, πειγραφή της λύσης, ανάλυση των οντοτήτων που λαμβάνουν μέρος, τις συσχετίσεις τους και παράδειγμα υλοποίησης.
Singleton
Στόχος: Κατασκευή κλάσης που επιτρέπει τη δημιουργία ενός μόνο instance αυτής.
Πρόβλημα: Έχουμε σχεδιάσει μία κλάση και θέλουμε οπουδήποτε χρησιμοποιείται instance αυτής της κλάσης να χρησιμοποποιείται πάντα το ίδιο.
Για παράδειγμα, έχουμε μία κλάση η οποία αποθηκεύει run-time ρυθμίσεις της εφαρμογής μας με το όνομα Registry. Από αυτές τις ρυθμίσεις εξαρτόνται πολλά κομμάτια της εφαρμογής. Δεν θέλουμε να χρησιμοποιήσουμε καθολικές μεταβλητές ούτε να περνάμε πάντα ένα instance της Registry σαν παράμετρο στους constructors όλων των υπόλοιπων κλάσεων της εφαρμογής.
Λύση: Απαγορεύουμε την άμεση δημιουργία instance της κλάσης. Δημιουργούμε μία μέθοδο για απόκτηση του instance η οποία πάντα επιστρέφει το ίδιο instance. Αν το instance δεν υπάρχει, το δημιουργεί και μετά πάντα επιστρέφει αυτό.
Αρχική υλοποίηση με C++:
Ας πούμε πως έχουμε την κλάση Registry. Για να απαγορεύσουμε την άμεση δημιουργία instance της Registry μετακινούμε τον constructor και τον destructor στο protected κομμάτι της κλάσης και δεν ορίζουμε assignment operator και copy constructor. Για την ανάκτηση instance ορίζουμε μία public και static μέθοδο getSingleton(). Για την αποθήκευση του instance ορίζουμε έναν private static δείκτη προς την Registry. Private για να μην τον βλέπει κανείς & static για να έχει πρόσβαση σε αυτόν η getSingleton().
Η δήλωση της κλάσσης Registry:
//registry.h
class Registry {
//Ο δείκτης που θα αποθηκεύει το καθολικό
//instance της κλάσσης
static Registry *_instance;
protected:
//Κάνουμε τον constructor & destructor protected
//ώστε να επιτρέπεται κατασκευή instance μόνο
//από κάποια μέθοδο της κλάσσης.
Registry(void);
~Registrty();
public:
//Επιστρέφει το καθολικό instance; Αν δεν υπάρχει
//το δημιουργεί πριν το επιστρέψει
static Registry *getSingleton(void);
//υπόλοιπες μέθοδοι της Registry
//...
//....
};
Αρχικά ο δείκτης είναι NULL. Οπότε, στην getSingleton() αν ο δείκτης είναι NULL δημιουργούμε το instance και στη συνέχεια πάντα επιστρέφουμε αυτό:
//registry.cpp
#include "registry.h"
//ορισμός της static μεταβλητής
Regitry *Registry::_instance=NULL;
//υλοποίηση της getSingleton()
Registry *Registry::getSingleton(void) {
if(Regisrty::_instance==NULL) {
//δεν υπάρχει instance; το δημιουργούμε
//o constructor καλείται εδώ και μόνο εδώ
//πουθενά αλλού στην εφαρμογή
Registry::_instance=new Registry();
}
return Regisrty::_instance;
}
Έτσι εκεί που θέλουμε να χρησιμοποιήσουμε την Registry πολύ απλά γράφουμε μία γραμμή κώδικα:
Registry *reg=Registry::getSingleton();
Τελική Υλοποίηση σε C++:
H παραπάνω αρχική υλοποίηση ναι μεν έλυσε το πρόβλημα αλλά δημιούρησε ένα άλλο. Αν παρατηρήσετε καλά στην getSingleton() κάνουμε ένα new την Regisrty. Δεν ορίσαμε όμως πουθενά τρόπο καταστροφής του instance ώστε να κληθεί ο destructor. Θα ήταν αφελές να σκεφτούμε, ε σιγά και τι έγινε μόνο ένα new που στην τελική η μνήμη θα αποδεσμευτεί κατά τον τερματισμό από το λειτουργικό σύστημα (υπάρχουν και λειτουργικά που δεν το κάνουν αυτό). Αυτό είναι κακή πρακτική και χαλάει την αισθητική του κώδικα.
Μία λύση θα ήταν να προσθέσουμε άλλη μία στατική μέθοδο, την Release() η οποία θα αναλάμβανε την καταστροφή του instance κατά τρόπο ανάλογο με την κατασκευή του από την getSingleton(). Δηλαδή, όταν δεν χρησιμοποιεί το instance πια κανένας άλλος να καλείται delete στον δείκτη.
Αυτό όμως προσθέτει αχρείαστη πολυπλοκότητα στην υλοποίηση και δημιουργεί μία μεγάλη τρύπα στον κώδικά μας. Γιατί α) θα πρέπει η getSingleton() και Release() να χρησιμοποιούν κάποιο μηχανισμό reference counting και β) κάθε κλήση της getSingleton() θα πρέπει να συνοδεύεται από αντίστοιχη κλήση της Release() και επειδή είμαστε άνθρωποι κανείς δε μας εγγυάται πως αυτό θα συμβαίνει πάντα.
Αν όλα αυτά σας τρομάζουν και σας κάνουν να σκεφτείτε πως καλύτερα να ζήσετε χωρίς το Singleton Design Pattern, μην ανησυχείτε η λύση είναι πάρα πολύ απλή και δεν απαιτεί πολλές αλλαγές στην προηγούμενη "τρύπια" υλοποίηση.
Η λύση βασίζεται στη λειτουργία των automatic pointers που ορίζονται στη standard c++ library. Αυτοί, είναι template κλάσεις που αναλαμβάνουν την "ιδιοκτησία" ενός δείκτη και όταν καλείται ο destructor τους, καλούν delete στον δείκτη που αποθηκεύουν.
Αν δεν είστε εξοικιωμένοι με τους automatic pointers ανατρέξτε σε κάποιο reference για περισσότερες πληροφορίες. Συνοπτικά, ο constructor των automatic pointers παίρνει σαν όρισμα έναν δείκτη στην heap (δλδ προερχόμενο από new), ο destructor τον κάνει delete, και με τη μέθοδο get() παίρνουμε την τιμή του.
Eπομένως, μετά και τις αλλαγές η Registry θα είναι:
//registry.h
//το header με τη δήλωση του automatic pointer
#include <memory>
class Registry {
//o αυτόματος δείκτης που αποθηκεύει
//το καθολικό instance
static std::auto_ptr<Registry> _auto_instance;
protected:
//Constructor & destructor όπως πριν
Registry(void);
~Registry();
public:
//όπως πριν
static Registry *getSingleton(void);
};
//registry.cpp
#include "registry.h"
//ορισμός του static auto_ptr
std::auto_ptr<Registry> Registry::_auto_instance;
//υλοποίηση της getSingleton()
Registry *Registry::getSingleton(void) {
//χρησιμοποιούμε την get() για να πάρουμε
//την τιμή του δείκτη που αποθηκεύει ο auto_ptr
if(Registry::_auto_instance.get()==NULL) {
//δεν υπάρχει instance το δημιουργούμε
//δεν μπορείες να κάνεις απευθείας ανάθεση
//ένα new σε ένα auto_ptr, μόνο auto_ptr
//σε άλλο auto_ptrοπότε δημιουργούμε
//ένα προσωρινό
std::auto_ptr<Registry> auto_temp(new Registry());
//και το αναθέτουμε στο _auto_instance
Registry::_auto_instance=auto_temp;
//το auto_temp μεταφέρει την ιδιοκτησία του δείκτη
//στο _auto_instance και δεν καλεί delete
//στον destructor του
}
}
Η υλοποίηση είναι σχεδόν ίδια. Η διαφορά είναι ότι με τον τερματισμό της εφαρμογής θα κληθούν οι destructors όλων των καθολικών και static μεταβλητών, οπότε αφού το _auto_instance είναι στατικό θα κληθεί ο destructor του και θα γίνει η καταστροφή του instance. Επομένως, δεν έχουμε αδέσμευτη μνήμη να καθαρίσει το λειτουργικό.
Το παραπάνω ήταν μόνο ένα παράδειγμα υλοποίησης του pattern. Φυσικά ο καθένας το προσαρμόζει στην εκάστοτε εφαρμογή αλλά η ιδέα είναι πάντα ίδια. Σίγουρα, σε όποια αντικειμενστραφή γλώσσα και δουλεύεται αργά ή γρήγορα θα σας παρουσιαστεί η ανάγκη εφαρμογής μίας τέτοιας αρχιτεκτονικής είτε είστε εξοικιωμένος με design patterns είτε όχι.
Αυτά περί Singleton. Mέχρι την επόμενη φορά που θα καλύψω κάποιο άλλο design pattern.