Σχεδιαστικά πρότυπα

Στην πληροφορική, ένα σχεδιαστικό πρότυπο ή σχεδιαστικό μοτίβο (design pattern) ορίζεται ως μία αποδεδειγμένα καλή λύση που έχει εφαρμοστεί με επιτυχία στην επίλυση ενός επαναλαμβανόμενου προβλήματος σχεδίασης συστημάτων λογισμικού. Τα πρότυπα σχεδίασης ορίζονται τόσο σε επίπεδο μακροσκοπικής σχεδίασης όσο και σε επίπεδο υλοποίησης, ενώ με τη χρήση τους ένας προγραμματιστής αντικαθιστά πρακτικώς μεγάλα τμήματα του κώδικα του με μαύρα κουτιά. Πρόκειται για αφαιρέσεις υψηλού επιπέδου που αποτελούν πλήρη υποσυστήματα, καταλλήλως ρυθμισμένα για την επίλυση συγκεκριμένων προβλημάτων σχεδίασης λογισμικού και έτοιμα για χρήση.

 Κατά τα τέλη της δεκαετίας του 1970 ένας αρχιτέκτονας ονόματι Κρίστοφερ Αλεξάντερ επιχείρησε να βρει και να καταγράψει αποδεδειγμένα ποιοτικούς σχεδιασμούς στον τομέα των κατασκευών. Έτσι μελέτησε πολλές διαφορετικές κατασκευές που εξυπηρετούσαν τον ίδιο σκοπό και προσπάθησε να ανακαλύψει κοινά στοιχεία, τα οποία κατηγοριοποίησε σε σχεδιαστικά πρότυπα. Το 1987 η ιδέα της εύρεσης σχεδιαστικών προτύπων εφαρμόστηκε για πρώτη φορά στη μηχανική λογισμικού και μέχρι τα μέσα της δεκαετίας του '90 η εν λόγω έννοια είχε καθιερωθεί και εξαπλωθεί, προσανατολισμένη πλέον προς τον αντικειμενοστρεφή προγραμματισμό. Ιδιαίτερα σημαντική εξέλιξη προς αυτή την κατεύθυνση υπήρξε η έκδοση του βιβλίου Design Patterns: Elements of Reusable Object-Oriented Software των Erich Gamma, Richard Helm, Ralph Johnson και John Vlissides, το 1994. Οι τέσσερις αυτοί συγγραφείς αναφέρονται στη βιβλιογραφία ως "Συμμορία των Τεσσάρων" (Gang of Four) λόγω του εν λόγω βιβλίου. Συνήθως τα σχεδιαστικά πρότυπα κατηγοριοποιούνται σε κατασκευαστικά (creational), δομικά (structural) και συμπεριφορικά (behavioral). Η κατηγοριοποίηση αυτή απαντάται ήδη από την εποχή της Συμμορίας των Τεσσάρων. Τα παραδείγματα κώδικα που παρέχονται παρακάτω είναι σε γλώσσα προγραμματισμού Java. Τα κατασκευαστικά πρότυπα αφορούν τυποποιημένους τρόπους δυναμικής κατασκευής αντικειμένων κατά τον χρόνο εκτέλεσης. Απώτερος στόχος τους είναι η ανεξαρτητοποίηση του κώδικα που χρησιμοποιεί κάποια αντικείμενα από τις κλάσεις που ορίζουν τα αντικείμενα αυτά και τον τρόπο που κατασκευάζονται στη μνήμη, σύμφωνα με την αρχή ανοιχτότητας-κλειστότητας για ορθή αντικειμενοστρεφή σχεδίαση. Ακολουθούν τα σπουδαιότερα κατασκευαστικά πρότυπα: Factory Το πρότυπο αυτό συγκεντρώνει σε μία κατάλληλη κλάση, το Factory, όλη τη λειτουργικότητα κατασκευής στιγμιοτύπων μίας σειράς παραγόμενων κλάσεων που κληρονομούν κάποια κοινή υπερκλάση ή υλοποιούν την ίδια διασύνδεση. Οι μέθοδοι του Factory, όταν καλούνται, κατασκευάζουν ένα νέο αντικείμενο κατάλληλου τύπου και επιστρέφουν έναν αφηρημένο δείκτη προς αυτό, δηλαδή έναν δείκτη τύπος του οποίου είναι η γενική διασύνδεση, οπότε το εξωτερικό πρόγραμμα μπορεί να τις καλεί για να λαμβάνει το ζητούμενο κατά περίπτωση αντικείμενο χωρίς το ίδιο να χρειάζεται να γνωρίζει κάθε παραγόμενο τύπο δεδομένων που υλοποιεί τη διασύνδεση. Με αυτόν τον τρόπο το πρόγραμμα είναι κλειστό ως προς πιθανές επεκτάσεις και μόνο το Factory είναι ανοιχτό ως προς αυτές, καθώς μόνον ο δικός του κώδικας χρειάζεται να τροποποιηθεί σε περίπτωση π.χ. προσθήκης μίας νέας παραγόμενης κλάσης. Το Factory συνήθως παρέχει μία μέθοδο για κάθε δυνατό παραγόμενο τύπο, αλλά μία παραλλαγή ονόματι παραμετροποιημένο Factory περιέχει μία μοναδική μέθοδο η οποία επιλέγει το αντικείμενο που θα δημιουργήσει και θα επιστρέψει, αναλόγως με την τιμή ενός ορίσματος που δέχεται. Το όρισμα αυτό μπορεί π.χ. να διαβάζεται από κάποιο αρχείο ρυθμίσεων ή να μεταβιβάζεται ως όρισμα γραμμής εντολών στο εξωτερικό πρόγραμμα (στον πελάτη), έτσι ώστε το τελευταίο να είναι κλειστό ως προς το σύνολο των παραγόμενων κλάσεων και να μη χρειάζεται επαναμεταγλώττιση σε περίπτωση που τροποποιηθεί το σύνολο αυτό. Ένας εναλλακτικός τύπος Factory είναι όταν ορίζεται ως αφηρημένη κλάση και η παρεχόμενη μέθοδος κατασκευής αντικειμένων υποσκελίζεται από υποκλάσεις του, έτσι ώστε η καθεμία από τις τελευταίες να κατασκευάζει αντικείμενο διαφορετικού τύπου. Σε κάθε περίπτωση στόχος είναι να μπορεί το πρόγραμμα να δημιουργεί στιγμιότυπα κλάσεων χωρίς να προσδιορίζει ρητά τον ακριβή τύπο τους και αφήνοντας το Factory να τον αποφασίσει εσωτερικά· το πρόγραμμα αρκεί να έχει γνώση του γενικού αφηρημένου τύπου. Υπάρχει ωστόσο και μία εναλλακτική, αρκετά διαφορετική περίπτωση όπου, αντί το Factory να δηλώνεται ως κάποια/ες ξεχωριστή/ες κλάση/εις, αποτελείται απλώς από μία ή περισσότερες στατικές μεθόδους της κλάσης της οποίας πρέπει να κατασκευάζει αντικείμενα (έστω της κλάσης Α). Τότε ο κατασκευαστής (constructor) της κλάσης δηλώνεται ως ιδιωτική μέθοδος ώστε κάθε απόπειρα του εξωτερικού προγράμματος να δημιουργήσει στιγμιότυπα της Α να γίνεται αναγκαστικά μέσω των μεθόδων Factory οι οποίες επιστρέφουν ένα νέο αντικείμενο τύπου Α. Σε αυτήν την περίπτωση όμως η Α δεν μπορεί να κληρονομηθεί, αφού για τον σκοπό αυτό πρέπει να υπάρχει τουλάχιστον ένας δημόσιος κατασκευαστής της. Ακολουθεί ένα απλό παράδειγμα παραμετροποιημένου Factory: Στον κώδικα αυτόν η κλάση LoggerFactory είναι η μόνη που γνωρίζει τους διαφορετικούς τύπους δεδομένων οι οποίοι υλοποιούν τη διασύνδεση Logger, άρα είναι και το μόνο σημείο του κώδικα που χρειάζεται να τροποποιηθεί σε περίπτωση που η Logger επεκταθεί με ακόμα μία παραγόμενη κλάση που την υλοποιεί. Το εξωτερικό πρόγραμμα, η κλάση LogTest, χειρίζεται απλώς ένα αντικείμενο του γενικού τύπου Logger και αφήνει στο Factory την απόφαση για τον πραγματικό τύπο του, αναλόγως με ένα όρισμα γραμμής εντολών που δίνεται από τον χρήστη. Ακολουθεί ένα παράδειγμα ορισμού του Factory ως μεθόδου μίας κλάσης την οποία πρέπει να παράγει: Abstract Factory Το Abstract Factory είναι ένα πρότυπο που αποτελεί παραλλαγή του Factory και χρησιμοποιείται όταν έχουμε παράλληλες κληρονομικές ιεραρχίες κλάσεων οι οποίες μοιράζονται κάποια κοινή ιδιότητα. Έστω π.χ. ότι γράφουμε μία βιβλιοθήκη για κατασκευή γραφικών διασυνδέσεων χρήστη (GUI) η οποία περιέχει διάφορα οπτικά στοιχεία (widgets) τα οποία κληρονομούν από μία κοινή διασύνδεση ονόματι Widget (π.χ. κουμπιά, μπάρες κύλισης κλπ). Υποθέτοντας ότι η βιβλιοθήκη υλοποιείται σε δύο εκδοχές, μία για λειτουργικό σύστημα Windows και μία για Unix, μπορούμε να δηλώσουμε ένα αφηρημένο Factory το οποίο παρέχει μεθόδους για την κατασκευή κάθε πιθανού οπτικού στοιχείου ώστε αυτές οι μέθοδοι να υποσκελίζονται διαφορετικά στις δύο παραγόμενες κλάσεις που υλοποιούν το αφηρημένο Factory: μία για Windows και μία για Unix. Ακολουθεί ο κώδικας του παραδείγματος: Όπως είναι φανερό από το παράδειγμα το εξωτερικό πρόγραμμα δε χρειάζεται να γνωρίζει τους ακριβείς τύπους δεδομένων WindowsButton, UnixButton, WindowsScrollBar και UnixScrollBar, καθώς το μόνο που χειρίζεται είναι οι αφηρημένοι τύποι Button και ScrollBar. Σε περίπτωση που είναι επιθυμητή η αλλαγή λειτουργικού συστήματος η μόνη γραμμή κώδικα που πρέπει να τροποποιηθεί είναι αυτή όπου δημιουργείται το αντικείμενο UnixFactory· αρκεί να γίνει WindowsFactory και τα κατασκευαζόμενα οπτικά στοιχεία θα "προσαρμοστούν" αυτομάτως χωρίς αλλαγές στον υπόλοιπο κώδικα. Η εναλλακτική λύση είναι να αξιοποιήσουμε δύο συνήθη Factory, όπου το ένα επιστρέφει τύπο Button και γνωρίζει εσωτερικά τους τύπους UnixButton και WindowsButton, ενώ το άλλο επιστρέφει τύπο ScrollBar και γνωρίζει εσωτερικά τους τύπους UnixScrollBar και WindowsScrollBar. Το Abstract Factory αποτελεί ουσιαστικώς μία "συγχώνευση" αυτών των δύο συνηθισμένων Factory, αφού κατασκευάζει τόσο κουμπιά όσο και μπάρες κύλισης, αλλά ταυτόχρονα και "διάσπασή" τους, αφού ορίζονται δύο παραγόμενα Factory: ένα για οπτικά στοιχεία του Unix και ένα για οπτικά στοιχεία των Windows. Αυτή η διάσπαση όμως δεν είναι απόλυτη αφού όλα τα παραγόμενα Factory υλοποιούν την ίδια διασύνδεση. Singleton Το Singleton είναι ένα σχεδιαστικό πρότυπο που επιλύει το ζήτημα της εξασφάλισης της ύπαρξης το πολύ ενός στιγμιότυπου κάποιας κλάσης Α κατά το χρόνο εκτέλεσης, ένας περιορισμός που μπορεί να ανακύψει για διάφορους λόγους. Με το πρότυπο Singleton η αρμοδιότητα για την ικανοποίηση αυτού του περιορισμού ανατίθεται στην ίδια την κλάση Α και δε μεταβιβάζεται στο εξωτερικό πρόγραμμα που τη χρησιμοποιεί. Αυτό γίνεται μέσω μίας στατικής δημόσιας μεθόδου της Α η οποία δημιουργεί το πρώτο στιγμιότυπο της κλάσης και ακολούθως, σε κάθε επόμενη κλήση της, επιστρέφει έναν δείκτη ή μία αναφορά προς αυτό. Το Singleton διαφέρει από μία απλή καθολική μεταβλητή (global variable), η οποία επίσης δημιουργείται αναγκαστικά μόνο μία φορά καθ' όλο το χρόνο εκτέλεσης του προγράμματος, καθώς το αντικείμενο Singleton δε δεσμεύει μνήμη μέχρι τη στιγμή της πρώτης κλήσης της μεθόδου κατασκευής. Από εκεί κι έπειτα όμως το αντικείμενο παραμένει στη μνήμη ως τον τερματισμό του προγράμματος γιατί είναι υλοποιημένο ως στατική μεταβλητή. Ακολουθεί ένα απλό παράδειγμα: Η Α περιέχει ένα στατικό γνώρισμα instance τύπου Α και μία στατική δημόσια μέθοδο getAObject η οποία ελέγχει αν στο γνώρισμα αυτό έχει ανατεθεί τιμή. Αν ναι επιστρέφει το ίδιο το γνώρισμα, διαφορετικά δημιουργεί ένα νέο στιγμιότυπο της ίδιας της κλάσης Α και το αναθέτει στο πεδίο instance προτού επιστρέψει το τελευταίο. Ο κατασκευαστής είναι ιδιωτικός ώστε να παρακάμπτεται αναγκαστικώς από το εξωτερικό πρόγραμμα και να χρησιμοποιείται πάντα η μέθοδος getAObject. Prototype Το Prototype είναι ένα σχεδιαστικό πρότυπο με το οποίο ένα νέο αντικείμενο κατασκευάζεται με "κλωνοποίηση" κάποιου υπάρχοντος. Η κλωνοποίηση αυτή γίνεται μέσω μίας μεθόδου clone η οποία παρέχεται από μία αφηρημένη κλάση ή διασύνδεση Α και υλοποιείται σε κάθε παραγόμενη κλάση Β η οποία κληρονομεί την Α. Έτσι η κλήση της clone σε ένα στιγμιότυπο της Β επιστρέφει ένα αντίγραφο του εν λόγω στιγμιότυπου, το οποίο αναλόγως με την υλοποίηση μπορεί να είναι είτε ρηχό, δηλαδή να περιέχει δείκτες προς τις εσωτερικές δομές δεδομένων του αρχικού στιγμιότυπου, είτε βαθύ, δηλαδή να περιέχει πλήρη, νεοδημιουργηθέντα αντίγραφα αυτών των δομών δεδομένων. Το Prototype χρειάζεται σε περιπτώσεις όπου πρέπει να κατασκευαστεί μία ρέπλικα ενός αντικειμένου αλλά με κάποιον άλλον τρόπο, π.χ. στην Java με χρήση του τελεστή new και ενός κατασκευαστή αντιγράφου (copy constructor), προσβάλλεται η αρχή ανοιχτότητας-κλειστότητας. Η μέθοδος clone μπορεί να επικαλύπτει την κλήση του εκάστοτε κατασκευαστή αντιγράφου με ένα κοινό επίπεδο αφαίρεσης έτσι ώστε να μη χρειάζεται το εξωτερικό πρόγραμμα να γνωρίζει όλους τους παραγόμενους τύπους δεδομένων που υλοποιούν τη διασύνδεση Α, καθώς η clone επιστρέφει αναφορά του αφηρημένου τύπου Α. Τα δομικά πρότυπα αφορούν τυποποιημένους τρόπους δυναμικής κατασκευής σύνθετων αντικειμένων τα οποία χρησιμοποιούν υπάρχουσες ιεραρχίες κλάσεων. Decorator Το πρότυπο αυτό επιτρέπει την εύκολη και δυναμική επέκταση της λειτουργικότητας κάποιων υπαρχόντων κλάσεων Α, Β κλπ, οι οποίες υλοποιούν την ίδια διασύνδεση ή κληρονομούν την ίδια αφηρημένη κλάση (έστω Interface), σε χρόνο εκτέλεσης. Αυτό γίνεται μέσω του Decorator, μίας νέας κλάσης η οποία επίσης υλοποιεί την Interface αλλά περιέχει ως ιδιωτικό πεδίο και μία αναφορά σε ένα στιγμιότυπο του γενικού τύπου Interface (έστω το instance), η οποία τυπικά μεταβιβάζεται ως όρισμα στον κατασκευαστή της Decorator. Έτσι οι μέθοδοι της τελευταίας υλοποιούν εσωτερικά την καινούργια λειτουργικότητα αλλά για τις κοινές εργασίες καλούν τις αντίστοιχες μεθόδους του instance. Κατά τον χρόνο εκτέλεσης θα μπορούσε το αντικείμενο Decorator να κατασκευάζεται με όρισμα οποιοδήποτε στιγμιότυπο τύπου Interface (ακόμα και του ίδιου του Decorator, αν και αυτό δε θα είχε ιδιαίτερο νόημα) ώστε κατά περίπτωση το αντικείμενο να παρέχει τη λειτουργικότητα οποιασδήποτε κλάσης τύπου Interface, είτε της Α είτε κάποιας άλλης, επεκτεταμένης με ένα συγκεκριμένο σύνολο δυνατοτήτων. Με αυτόν τον τρόπο γίνεται εφικτός ένας δυναμικός συνδυασμός λειτουργιών από στοιχειώδεις δομικούς λίθους κατά τον χρόνο εκτέλεσης. Η εναλλακτική λύση, χωρίς χρήση κάποιου σχεδιαστικού προτύπου, θα ήταν η απλή κληρονομικότητα, με τον ορισμό κλάσεων οι οποίες επεκτείνουν τις Α, Β κλπ. και προσθέτουν τη νέα λειτουργικότητα. Ωστόσο η λύση αυτή δεν είναι εφικτή σε περίπτωση που οι Α, Β κλπ. δεν μπορούν να επεκταθούν με κληρονομικότητα (π.χ. αν δηλωθούν ως τελικές κλάσεις στην Java), ενώ σε άλλες περιπτώσεις δεν είναι καθόλου πρακτική, π.χ. αν έχουμε πολλαπλά διαφορετικά σύνολα νέων δυνατοτήτων τα οποία πρέπει να συνδυαστούν με τις Α, Β κλπ. Το πρόβλημα έγκειται στο ότι με την κληρονομικότητα όλοι οι πιθανοί συνδυασμοί δυνατοτήτων πρέπει να προβλεφθούν και να ληφθούν υπ’ όψιν κατά τη συγγραφή του προγράμματος. Αντιθέτως με την κλάση Decorator, η οποία δρα ως περίβλημα (wrapper) άλλων αντικειμένων τύπου Interface προς τα οποία περιέχει αναφορές / δείκτες, η σύνθεση νέων αντικειμένων ουσιαστικά γίνεται δυναμικά ενώ το πρόγραμμα εκτελείται. Ακολουθεί ένα απλό παράδειγμα: Adapter Το πρότυπο αυτό επιτρέπει την προσαρμογή της διασύνδεσης που εξάγει μία κλάση Α σε μία πρότυπη διασύνδεση, έστω Interface, που αναμένεται από ένα εξωτερικό πρόγραμμα ώστε να μην παραβιάζεται η αρχή ανοιχτότητας-κλειστότητας. Αυτό γίνεται μέσω μίας ενδιάμεσης κλάσης, του Adapter, η οποία υλοποιεί το Interface και ταυτοχρόνως είτε κληρονομεί την Α (υλοποίηση με κληρονομικότητα), είτε περιέχει μία ιδιωτική αναφορά σε αντικείμενο τύπου Α (υλοποίηση με συνάθροιση). Σε κάθε περίπτωση ο Adapter έχει πρόσβαση στις μεθόδους της Α και οι δικές του μέθοδοι, οι υπογραφές των οποίων συμφωνούν με αυτές που αναμένονται από κάποιον πελάτη καθώς προδιαγράφονται από τη διασύνδεση / αφηρημένη κλάση Interface, τις καλούν εσωτερικά και επιστρέφουν τα αποτελέσματα, αποκρύπτοντας παράλληλα τη διαδικασία αυτή από το εκάστοτε πρόγραμμα πελάτη. Το πρόβλημα που επιλύει το εν λόγω σχεδιαστικό πρότυπο εν γένει προκύπτει όταν μία υπάρχουσα ιεραρχία κλάσεων, οι οποίες υλοποιούν ένα συγκεκριμένο στοιχείο αφαίρεσης, πρέπει να επεκταθεί με την προσθήκη μίας νέας κλάσης η διασύνδεση της οποίας δεν είναι συμβατή με το υπάρχον στοιχείο αφαίρεσης. Η υλοποίηση του προτύπου Adapter μέσω κληρονομικότητας επιτρέπει την προσαρμογή στη ζητούμενη διασύνδεση και των προστατευμένων μεθόδων της κλάσης Α, αντί μόνο των δημοσίων της, ενώ είναι και ελαφρώς πιο αποδοτική από την υλοποίηση με συνάθροιση, αφού κάθε κλήση στον Adapter «μεταφράζεται» σε κλήση σε μια άλλη μέθοδο του ιδίου αντί για κλήση σε μέθοδο άλλου αντικειμένου. Από την άλλη η υλοποίηση με συνάθροιση δεν αντιμετωπίζει προβλήματα ακόμα και αν η Α δεν μπορεί να κληρονομηθεί, ενώ επιτρέπει την προσαρμογή όχι μόνο της Α αλλά και κάθε υποκλάσης της λόγω του πολυμορφισμού. Bridge Σε περιπτώσεις που έχουμε ένα στοιχείο αφαίρεσης Α (π.χ. αφηρημένη κλάση ή διασύνδεση) και κάποιες κλάσεις που το υλοποιούν, είναι εύκολη η επέκταση της εν λόγω ιεραρχίας με ακόμα μία παραγόμενη κλάση χωρίς ιδιαίτερο κόπο· αρκεί το εξωτερικό πρόγραμμα να χρησιμοποιεί αναφορές προς τον αφηρημένο τύπο Α και όχι προς τους επιμέρους εξειδικευμένους παραγόμενους τύπους (αρχή ανοιχτότητας-κλειστότητας και αξιοποίηση του πολυμορφισμού). Όμως δεν είναι εύκολη η επέκταση της ίδιας της διασύνδεσης Α με νέες μεθόδους, ιδιότητες κλπ., έστω με ένα καινούργιο στοιχείο αφαίρεσης Β το οποίο κληρονομεί το Α, καθώς οι πιθανές κλάσεις που θα υλοποιούν το Β δεν είναι εύκολο να εκμεταλλευτούν τον υπάρχοντα κώδικα αντίστοιχων κλάσεων οι οποίες υλοποιούν το παλαιό στοιχείο αφαίρεσης Α. Θα μπορούσε βέβαια αυτό να γίνει με πολλαπλή κληρονομικότητα αλλά η εν λόγω σχεδίαση δεν είναι ευέλικτη και δεν υποστηρίζεται πάντοτε. Τη λύση δίνει το πρότυπο Bridge, το οποίο χρησιμοποιείται όταν το υπό ανάπτυξη σύστημα περιέχει στοιχεία αφαίρεσης από τα οποία μπορούν να προκύψουν διαφορετικές υλοποιήσεις αλλά και νέα στοιχεία αφαίρεσης, που επίσης μπορούν να υλοποιηθούν σε διάφορες παραγόμενες κλάσεις. Η εφαρμογή του προτύπου έγκειται στη σχεδίαση δύο ιεραρχιών κληρονομικότητας, μίας ιεραρχίας στοιχείων αφαίρεσης (υψηλού επιπέδου) και μίας ιεραρχίας υλοποιήσεων (χαμηλού επιπέδου). Οι κλάσεις χαμηλού επιπέδου χρησιμοποιούνται για την οικοδόμηση των κλάσεων υψηλού επιπέδου μέσω συνάθροισης, καθώς το βασικό στοιχείο αφαίρεσης περιέχει μία αναφορά σε γενικού τύπου στιγμιότυπο του βασικού στοιχείου της ιεραρχίας χαμηλού επιπέδου. Έτσι οποιαδήποτε κλάση της ιεραρχίας υψηλού επιπέδου μπορεί να κάνει χρήση αντικειμένων οποιασδήποτε κλάσης της ιεραρχίας χαμηλού επιπέδου και αλλαγές μπορούν να γίνουν κατά τον χρόνο εκτέλεσης. Οι μέθοδοι των κλάσεων υψηλού επιπέδου μπορούν να κάνουν χρήση μόνο των μεθόδων του βασικού στοιχείου της ιεραρχίας χαμηλού επιπέδου οι οποίες υποσκελίζονται στις επιμέρους υλοποιήσεις. Ακολουθεί ένα απλό παράδειγμα, όπου ένα στοιχείο αφαίρεσης Α υλοποιείται με δύο διαφορετικούς τρόπους (μία παραγόμενη κλάση για Windows και μία για Unix). Σκοπός είναι η επέκταση της Α με ένα στοιχείο αφαίρεσης Β το οποίο κληρονομεί από αυτήν και έχει τις δικές του υλοποιήσεις. Με αυτόν τον στόχο κατά νου γίνεται χρήση του προτύπου Bridge: Facade Σε περίπτωση που, για την εκτέλεση μίας εργασίας, διαθέσιμο είναι ένα περίπλοκο σύνολο αλληλεπιδρώντων κλάσεων (έστω σύνολο Α) οι οποίες συνδυάζονται με διάφορους τρόπους, τότε ο προγραμματιστής μπορεί να αποκρύψει αυτόν τον ιστό αλληλεπιδράσεων "καλύπτοντας" τις επίμαχες μονάδες λογισμικού με μία κλάση η οποία εξάγει μία απλή και εύχρηστη διασύνδεση, ένα σύνολο κατάλληλων δημοσίων μεθόδων το οποίο χρησιμοποιεί εσωτερικά το σύνολο Α. Η διασύνδεση αυτή μπορεί ακολούθως να αξιοποιηθεί για την παραγωγή νέου κώδικα χωρίς ανάγκη άμεσης καταφυγής στις περίπλοκες κλάσεις του Α, αποτελεί δηλαδή μία "βιτρίνα" υψηλού επιπέδου. Ένα μη αντικειμενοστρεφές παράδειγμα εφαρμογής του προτύπου Facade είναι ορισμένες συναρτήσεις της πρότυπης βιβλιοθήκης της C οι οποίες αποτελούν απλουστευμένες εκδοχές αντίστοιχων κλήσεων συστήματος του Unix (π.χ. η συνάρτηση fopen επικαλύπτει την κλήση open). Proxy Όταν η πρόσβαση ενός εξωτερικού προγράμματος σε αντικείμενα κάποιας συγκεκριμένης κλάσης (έστω της Α) πρέπει να είναι ελεγχόμενη και να πληροί ορισμένες προϋποθέσεις (π.χ., για λόγους εξοικονόμησης μνήμης, να μη φορτώνεται πραγματικά η Α μέχρι να κληθεί μία μέθοδος της), το πρότυπο Proxy παρέχει έναν τυποποιημένο τρόπο ώστε αυτό να γίνεται διατηρώντας την κλειστότητα του εξωτερικού προγράμματος ως προς την εν λόγω κλάση και τον τρόπο λειτουργίας της· ακόμα και αν η υλοποίηση της αλλάξει ο πελάτης είναι αδιάφορος απέναντι σε αυτές τις αλλαγές (δε χρειάζεται τροποποίηση) χάρη σε ένα επίπεδο αφαίρεσης που του παρέχεται από μία ενδιάμεση κλάση, τον Proxy, η οποία παίζει το ρόλο του μεσολαβητή πρόσβασης. Ο Proxy είναι που εκτελεί όλους τους απαραίτητους ελέγχους και προσπελαύνει πραγματικά τα αντικείμενα, ενώ ταυτόχρονα υλοποιεί την ίδια διασύνδεση με την Α κι έτσι ο πελάτης, ο οποίος κατέχει μία αναφορά προς στιγμιότυπο του Proxy αντί της Α, δεν αντιλαμβάνεται καν τη μεσολάβησή του· ο Proxy ουσιαστικώς εικονικοποιεί την πρόσβαση στη ζητούμενη κλάση. Συνήθως κατασκευάζεται, αντί για την Α, από ένα Factory και εντελώς διαφανώς για το εξωτερικό πρόγραμμα, ενώ δεν είναι σπάνιο να περιέχει μία αναφορά στο πραγματικό στιγμιότυπο της Α ως ιδιωτικό πεδίο. Το σχεδιαστικό αυτό πρότυπο αποτελεί ειδική εφαρμογή της γενικότερης έννοιας του proxy στην πληροφορική. Με αυτήν τη γενικότερη έννοια ο proxy μπορεί να μεσολαβεί μεταξύ ενός πελάτη και ενός οποιουδήποτε ακριβού, σπάνιου ή περίπλοκου πόρου. Μία ιδιαίτερη περίπτωση εφαρμογής της έννοιας του proxy αποτελεί ο απομακρυσμένος proxy, όπως τα στελέχη (stubs) πελάτη και διακομιστή στο υπόδειγμα απομακρυσμένης κλήσης διαδικασιών στα συστήματα ενδιάμεσου λογισμικού. Ακολουθεί ένα παράδειγμα όπου μία κλάση τοπικού Proxy αξιοποιείται έτσι ώστε, αν το επιλέξει ο χρήστης, να καθυστερεί τη φόρτωση στη μνήμη της κλάσης LargeClass μέχρι να κληθεί μία μέθοδός της: Τα συμπεριφορικά πρότυπα αφορούν τον καταμερισμό αρμοδιοτήτων σε διάφορες κλάσεις και τον ορισμό του τρόπου επικοινωνίας μεταξύ των αντικειμένων τους κατά τον χρόνο εκτέλεσης. Σε αντίθεση με τα δομικά πρότυπα, τα συμπεριφορικά βρίσκουν εφαρμογή στον αρχικό σχεδιασμό μίας ιεραρχίας κλάσεων και όχι στην εκ των υστέρων επέκταση κάποιας υπάρχουσας ιεραρχίας. Ο γενικός κανόνας είναι ότι έχουμε να κάνουμε με μία ομάδα κλάσεων οι οποίες υλοποιούν με διαφορετικούς τρόπους μία κοινή διασύνδεση Α· ως συνήθως το εξωτερικό πρόγραμμα είναι προτιμότερο να χειρίζεται μόνον αναφορές του αφηρημένου τύπου Α και να βασίζεται στον πολυμορφισμό ώστε να μην παραβιάζεται η αρχή ανοιχτότητας-κλειστότητας. Η επικοινωνία μεταξύ αντικειμένων γίνεται με παρόμοιο τρόπο, καθώς όχι σπάνια μία κλήση μεθόδου (ίσως κληση κατασκευαστή) δέχεται ως ορισμα μία αναφορά αφηρημένου τύπου κι έτσι το αντίστοιχο αντικείμενο μπορεί να προσπελαύνει με τυποποιημένο τρόπο δημόσιες μεθόδους στιγμιοτύπων κάθε κλάσης της επίμαχης ιεραρχίας. Strategy Όταν είναι διαθέσιμοι πολλαπλοί τρόποι επίλυσης ενός προβλήματος (π.χ. διαφορετικοί αλγόριθμοι), είναι προτιμότερο ο καθένας από αυτούς να μην υλοποιείται μέσα στις κλάσεις-πελάτες που τον χρησιμοποιούν (π.χ. ως ιδιωτική μέθοδος), έτσι ώστε οι πελάτες να έχουν μικρότερη περιπλοκότητα και οι διάφοροι αλγόριθμοι να είναι επαναχρησιμοποιήσιμοι και προσπελάσιμοι από πολλαπλά εξωτερικά προγράμματα. Με το πρότυπο Strategy όλοι οι διαφορετικοί αλγόριθμοι ορίζονται ως ξεχωριστές κλάσεις που υλοποιούν μία κοινή διασύνδεση Α και οι πελάτες διατηρούν ως πεδίο μία αναφορά προς τον αφηρημένο τύπο Α. Στο πεδίο αυτό δίνεται τιμή μέσω του ορίσματος κάποιας μεθόδου του πελάτη (πιθανώς του κατασκευαστή του), έτσι ώστε η ταυτότητα του εκάστοτε χρησιμοποιούμενου αλγορίθμου από όλους τους διαθέσιμους για την εκτέλεση της ζητούμενης εργασίας να είναι εύκολα παραμετροποιήσιμη, με μία απλή αλλαγή αυτού του ορίσματος κατά την κλήση της προαναφερθείσας μεθόδου. Template Το πρότυπο αυτό είναι εκλέπτυνση του Strategy για την περίπτωση που κάθε αλγόριθμος έχει πολλαπλές παραλλαγές οι οποίες διαφέρουν σε ορισμένα βήματα αλλά κάποια άλλα σημεία τους είναι κοινά. Τότε για κάθε αλγόριθμο μπορούμε να ορίσουμε ένα στοιχείο αφαίρεσης Β το οποίο υλοποιεί τη διασύνδεση Α και με τη σειρά του κληρονομείται από πολλές παραλλαγές του αλγορίθμου. Το κάθε Β περιέχει την υλοποίηση των αμετάβλητων βημάτων και, στα σημεία που οι παραλλαγές διαφέρουν, καλεί αφηρημένες προστατευμένες μεθόδους. Οι μέθοδοι αυτές υλοποιούνται διαφορετικά σε κάθε παραλλαγή που κληρονομεί το Β. Visitor Το πρότυπο Visitor είναι ένας τρόπος διαχωρισμού κάποιου αλγορίθμου, ο οποίος πιθανώς εκφράζει μία επέκταση στη λειτουργικότητα μίας ιεραρχίας κλάσεων, από τις εν λόγω κλάσεις. Στόχος είναι η σταδιακή προσθήκη νέων δυνατοτήτων σε όλες τις κλάσεις που υλοποιούν μία διασύνδεση Α χωρίς να χρειάζεται να τροποποιηθούν οι κλάσεις αυτές και χωρίς το εξωτερικό πρόγραμμα να γνωρίζει τους ακριβείς τύπους των αντικειμένων (μπορεί να χειρίζεται τα τελευταία με αφηρημένες αναφορές τύπου Α). Το μέσον για την επίτευξη του σκοπού αυτού είναι η ομαδοποίηση όλων των μεθόδων που περιγράφουν τη νέα λειτουργικότητα για κάθε κλάση της ιεραρχίας σε μία νέα, ξεχωριστή κλάση Visitor η οποία δεν κληρονομεί από την Α. Η Visitor περιέχει πολυμορφικές μεθόδους visit η καθεμία από τις οποίες δέχεται διαφορετικού τύπου όρισμα· οι πιθανοί τύποι είναι οι παραγόμενες κλάσεις που υλοποιούν την Α. Κάθε μέθοδος visit περιγράφει τη νέα λειτουργικότητα που αντιστοιχεί στην κλάση του ορίσματος της (π.χ. μία επεξεργασία των δεδομένων της εν λόγω κλάσης) αλλά οι μέθοδοι αυτές δεν καλούνται άμεσα από τον προγραμματιστή. Αντιθέτως το εξωτερικό πρόγραμμα, για κάθε αντικείμενο Β της ιεραρχίας στο οποίο θέλει να προσθέσει τη νέα λειτουργικότητα, καλεί μία μέθοδο accept του Β με όρισμα το αντικείμενο Visitor. Η accept πρέπει να προδιαγράφεται από την Α και να υλοποιείται σε κάθε παραγόμενη κλάση, ενώ το μόνον που κάνει είναι να καλεί με τη σειρά της τη μέθοδο visit του ορίσματος της με όρισμα το αντικείμενο όπου περιέχεται. Έτσι τελικώς γίνεται η κατάλληλη επεξεργασία, με κλήση της κατάλληλης μεθόδου, χωρίς το εξωτερικό πρόγραμμα να γνωρίζει καν τον ακριβή παραγόμενο τύπο της εκάστοτε Β. Αν πολλές διαφορετικές επεκτάσεις είναι επιθυμητές, τότε μπορεί η κλάση Visitor να γίνει διασύνδεση και να υλοποιείται με διαφορετικές κλάσεις για κάθε ζητούμενη επέκταση. Observer Σε περιπτώσεις που κάποια αντικείμενα (παρατηρητές) ενδιαφέρονται να λαμβάνουν ειδοποιήσεις για τυχόν αλλαγές στην κατάσταση κάποιου άλλου αντικειμένου Α (π.χ. τροποποιήσεις τιμών κάποιων πεδίων του), υπάρχουν δύο προσεγγίσεις για την υλοποίηση αυτών των ενημερώσεων: είτε τις κατάλληλες στιγμές το Α να καλεί προκαθορισμένες μεθόδους των παρατηρητών και να τους μεταβιβάζει έτσι δεδομένα, είτε οι παρατηρητές να καλούν μία μέθοδο του Α για να λαμβάνουν κατά βούληση πληροφορίες. Η ενδιαφέρουσα περίπτωση είναι η πρώτη καθώς μόνον το Α γνωρίζει πότε υπάρχουν αλλαγές στην κατάσταση του και τέτοιες αλλαγές είναι που πρέπει να πυροδοτούν τις ενημερώσεις. Ο απλούστερος τρόπος είναι να διατηρεί μία συνδεδεμένη λίστα με όλους τους παρατηρητές που κατά καιρούς έχουν εκδηλώσει ενδιαφέρον και να καλεί τις κατάλληλες μεθόδους τους τις κατάλληλες στιγμές. Όμως αυτή η λύση προκαλεί προβλήματα κλειστότητας καθώς ένας άλλος τύπος παρατηρητή μπορεί να έχει διαφορετικές μεθόδους, ενώ το Α παρουσιάζει υψηλή σύζευξη με άλλες κλάσεις. Το πρότυπο Observer δίνει μία λύση σε αυτό το πρόβλημα: ορίζει μία διασύνδεση Observable, η οποία περιέχει μία μέθοδο register (για την εκούσια δήλωση παρατηρητών) και μία μέθοδο unregister (για την εκούσια αποχώρηση παρατηρητών), και μία διασύνδεση Observer, με μία μέθοδο synchronize για τη μεταβίβαση ενημερωμένων δεδομένων. Η κλάση κάθε αντικειμένου Α πρέπει να υλοποιεί τη διασύνδεση Observable και η κλάση κάθε παρατηρητή τη διασύνδεση Observer. Ένα αντικείμενο Observable διατηρεί μία λίστα με αντικείμενα του αφηρημένου τύπου Observer τα οποία έχουν καλέσει τη μέθοδο register του εν λόγω αντικειμένου. Οι εγγραφές αυτές χρησιμοποιούνται τις κατάλληλες χρονικές στιγμές για την ειδοποίηση των παρατηρητών και αυτό το είδος αλληλεπίδρασης ονομάζεται publish-subscribe.Ένα παράδειγμα χρήσης του πρότυπου Objerver είναι ένα κουμπί σε ένα γραφικό περιβάλλον εργασίας. Χρειαζόμαστε ένα αντικείμενο objserver το οποίο θα παρακολουθεί το κουμπί και όταν αυτό θα πατηθεί να ενεργοποιεί μια εκτέλεση κώδικα. Στην java τέτοια αντικείμενα λέγονται και listeners. Erich Gamma ... [et al.] (1995). Design patterns : elements of reusable object-oriented software. Reading, Mass.: Addison-Wesley. ISBN 9780201633610. Dalmau, Daniel Sanchez-Crespo (2004). Core techniques and algorithms in game programming ([Nachdr.] έκδοση). Indianapolis, Ind. [u.a.]: New Riders. ISBN 978-0131020092. Ζάρρας, Απόστολος. «Σχεδιαστικά Πρότυπα». Πανεπιστήμιο Ιωαννίνων.