Συνεχίζοντας την περιήγησή μας στα design patterns και πως αυτά υλοποιούνται σε C++, θα ασχοληθούμε με ένα άλλο pattern το οποίο αφορά τη δημιουργία αντικειμένων. To Prototype Design Pattern.
Δεν είναι αναγκαστικό να διαβάσετε το προηγούμενο post που αφορούσε το Singleton Pattern αλλά θα σας το συνιστούσα. Συνήθως για την υλοποίηση ή χρήση ενός pattern χρειαζόμαστε ένα άλλο και οπότε όσα παρουσιάσω θα είναι με μία λογική σειρά.
Σκοπός:
Προσδιορισμός του είδος των αντικειμένων που θα δημιουργούνται με βάση ένα προτότυπο instance μίας κλάσσης και στη συνέχεια δημιουργίας αντίγραφου αυτού.
Πρόβλημα:
Έχουμε διάφορες υλοποιήσεις μιας abstract κλάσης. Κατά την εκτέλεση της εφαρμογής μας θέλουμε να δημιουργούμε αντικείμενα μιας συγκεκριμένης υλοποίησης της abstract κλάσης, δηλαδή αντικείμενα μίας συγκεκριμένης concrete κλάσης. Το ποιά concrete κλάση θα χρησιμοποιείται για τη δημιουργία των αντικειμένων μπορεί να ορίζεται από τις παραμέτρους της εφαρμογής, από κάποια ρύθμιση και ενδεχομένως αυτή η ρύθμιση μπορεί να αλλάζει και κατά την εκτέλεση της εφαρμογής. Μία πολύ άσχημη λύση είναι κάθε φορά που θέλουμε να δημιουργίσουμε το αντικείμενο να το κάνουμε σε ένα switch ή σε ένα if με όλες τις concrete κλάσεις.
Λύση:
Σε κάθε concrete κλάση έχουμε μία μέθοδο Clone() η οποία επιστρέφει αντίγραφο του εαυτού της. Ορίζουμε μία κλάση με την οποία θα δημιουργούμε τα αντικείμενα. O constructor αυτής θα παίρνει σαν παράμετρο ένα προτότυπο instance της abstract κλάσης. Κατά τη δημιουργία ενός αντικειμένου η κλάση θα δημιουργεί ένα αντίγραφο του προτότυπου instance μέσω της Clone().
Yλοποίηση σε C++:
Για παράδειγμα ας πούμε πως αναπτύσουμε έναν html editor. Η εφαρμογή μας μπορεί να δημιουργεί δύο είδη html αρχείων: html και xhtml. Υπάρχει και μία ρύθμιση για το default είδος αρχείου. Έτσι κάθε φορά που στην εφαρμογή θέλουμε να δημιουργήσουμε ένα καινούργιο αρχείο θα πρέπει να δημιουργείται το default.
Ας πούμε πως το interface ενός html αρχείου ορίζεται από την abstract κλάση Document:
//document.h
//Η abstract κλάση που ορίζει το interface των αρχείων
//που χειρίζεται η εφαρμογή
class Document {
public:
//Οι μέθοδοι του interface
virtual void Preview(void)=0;
//...
//...
//Μέθοδος κλωνοποίησης. Επιστρέφει αντίγραφο
//του instance στο οποίο καλείται η μέθοδος
virtual Document *Clone(void)=0;
};
Ορίζουμε τις concrete υποκλάσεις της Document για κάθε είδος αρχείου που χειρίζεται η εφαρμογή μας. Σε κάθε μία ορίζουμε και μία μέθοδο Clone() που επιστρέφει δείκτη σε ένα αντίγραφο του εαυτού της.
//filetypes.h
#include "document.h"
//concrete υποκλάση της Document για html αρχεία
class HtmlFile: public Document {
public:
//Υλοποιήσεις των μεθόδων του interface
void Preview(void);
//...
//...
//Μέθοδος κλωνοποίησης. Επιστρέφει αντίγραφο
//του instance στο οποίο καλείται η μέθοδος
Document *Clone(void) {
return new HtmlFile(*this);
}
};
//concrete υποκλάση της Document για html αρχεία
class XHtmlFile: public Document {
public:
//Υλοποιήσεις των μεθόδων του interface
void Preview(void);
//...
//...
//Μέθοδος κλωνοποίησης. Επιστρέφει αντίγραφο
//του instance στο οποίο καλείται η μέθοδος
Document *Clone(void) {
return new XHtmlFile(*this);
}
};
Παρατηρήστε την κάθε Clone(). Δημιουργούμε ένα καινούργιο concrete αντικείμενο καλώντας τον copy constructor για να αντιγραφτεί το instance στο οποίο έγινε η κλήση. Εδώ θέλει λίγη προσοχή. Χάρην παραδείγματος δεν ορίσαμε copy constructors στις κλάσεις HtmlFile και XHtmlFile οπότε θα γίνει shallow copy των instances, δηλαδή θα αντιγραφούν και πιθανοί δείκτες που αποθηκεύει το αντικείμενο χωρίς να αντιγραφούν όμως τα αντικείμενα στα οποία δείχνουν. Σε τέτοιες περιπτώσεις, που είναι και η πλειοψηφία, θα πρέπει να ορίζεται copy constructor που κάνει deep copy των αντικειμένων.
Στη συνέχεια δημιουργούμε και μία κλάση που θα δημιουργεί αντικείμενα τύπου Document. Ας την ονομάσουμε DocumentFactory. Για να καθοριστεί ο concrete τύπος των αντικειμένων που θα δημιουργούνται η DocumentFactory θα αποθηκεύει ένα πρωτότυπο instance της concrete κλάσης στο private κομμάτι. Η μέθοδος CreateDocument() θα δημιουργεί ένα αντικείμενο τύπου Document καλώντας την Clone() στο πρωτότυπο instance. Οπότε, με αυτόν τον τρόπο κάθε φορά που θα καλείται η CreateDocument() θα δημιουργείται ένα αντικείμενο του ίδιου τύπου με το πρωτότυπο.
//document.h
//Δημιουργεί νέα Documents αντιγράφοντας το prototype
class DocumentFactory {
//αποθηκεύει το πρωτότυπο instance
Document *_prototype;
public:
//Δημιουργεί και ρυθμίζει το factory
//με το πρωτότυπο που θα χρησιμοποιείται
DocumentFactory(Document *proto) {
_prototype=proto;
}
//Copy Constructor
//Σε περίπτωση που αλλάζει η default ρύθμιση
DocumentFactory &DocumentFactory(const DocumentFactory &factory) {
_prototype=factory.prototye;
delete factory.prototype;
}
//destructor
~DocumentFactory() {
delete _prototype;
}
//Δημιουργία νέου Document
//κλωνοποιώντας το πρωτότυπο
Document *CreateDocument(void) {
return _prototype->Clone();
}
};
Οπότε, όταν ξεκινάει η εφαρμογή του editor και αφού διαβαστεί η default ρύθμιση για το είδος των νέων αρχείων θα μπορούσαμε να κάνουμε το εξής:
Registry *reg=Registry::getSingleton();
DocumentFactory *factory=reg->getDocumentFactory();
if(reg->getDefaultType()==std::string("html"))
factory=new DocumentFactory(new HtmlFile());
else
factory=new DocumentFactory(new XHtmlFile());
Σε αυτό το code fragment κάνουμε χρήση του Registry που ορίσαμε στο προηγούμενο design pattern που καλήψαμε, το Singleton, δείτε εδώ.
Το Prototype Design Pattern χρησιμοποιείται κυρίως σε άλλα desgin patterns που επιλύουν προβλήματα δημιουργίας αντικειμένων όπως το Abstract Factory. Οι σημαντικότερες επιδράσεις του Prototype είναι μείωση του αριθμού των κλάσεων και αύξηση των δυνατοτήτων δυναμικής ρύθμισης συμπεριφοράς όπου εφαρμόζεται.
Αυτό σχετικά με το Prototype και την υλοποίησή του σε C++. Την επόμενη φορά θα προσπαθήσω να καλύψω άλλο ένα pattern που λύνει προβλήματα δημιουργίας αντικειμένων.
Ελπίζω πως αυτή θα είναι η πρώτη δημοσίευση σε μία σειρά που θα ασχολείται με 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.