Σχεδιαστικά πρότυπα
Στην πληροφορική, ένα σχεδιαστικό πρότυπο ή σχεδιαστικό μοτίβο (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.
Τα κατασκευαστικά πρότυπα αφορούν τυποποιημένους τρόπους δυναμικής κατασκευής αντικειμένων κατά τον χρόνο εκτέλεσης.
Απώτερος στόχος τους είναι η ανεξαρτητοποίηση του κώδικα που χρησιμοποιεί κάποια αντικείμενα από τις κλάσεις που ορίζουν τα αντικείμενα αυτά και τον τρόπο που κατασκευάζονται στη μνήμη, σύμφωνα με την αρχή ανοιχτότητας-κλειστότητας για ορθή αντικειμενοστρεφή σχεδίαση.
Ακολουθούν τα σπουδαιότερα κατασκευαστικά πρότυπα:
right|400px|thumb|UML: Το Σχεδιαστικό Πρότυπο Factory.
Το πρότυπο αυτό συγκεντρώνει σε μία κατάλληλη κλάση, το Factory, όλη τη λειτουργικότητα κατασκευής στιγμιοτύπων μίας σειράς παραγόμενων κλάσεων που κληρονομούν κάποια κοινή υπερκλάση ή υλοποιούν την ίδια διασύνδεση.
Οι μέθοδοι του Factory, όταν καλούνται, κατασκευάζουν ένα νέο αντικείμενο κατάλληλου τύπου και επιστρέφουν έναν αφηρημένο δείκτη προς αυτό, δηλαδή έναν δείκτη τύπος του οποίου είναι η γενική διασύνδεση, οπότε το εξωτερικό πρόγραμμα μπορεί να τις καλεί για να λαμβάνει το ζητούμενο κατά περίπτωση αντικείμενο χωρίς το ίδιο να χρειάζεται να γνωρίζει κάθε παραγόμενο τύπο δεδομένων που υλοποιεί τη διασύνδεση.
Με αυτόν τον τρόπο το πρόγραμμα είναι κλειστό ως προς πιθανές επεκτάσεις και μόνο το Factory είναι ανοιχτό ως προς αυτές, καθώς μόνον ο δικός του κώδικας χρειάζεται να τροποποιηθεί σε περίπτωση π.χ. προσθήκης μίας νέας παραγόμενης κλάσης.
Το Factory συνήθως παρέχει μία μέθοδο για κάθε δυνατό παραγόμενο τύπο, αλλά μία παραλλαγή ονόματι παραμετροποιημένο Factory περιέχει μία μοναδική μέθοδο η οποία επιλέγει το αντικείμενο που θα δημιουργήσει και θα επιστρέψει, αναλόγως με την τιμή ενός ορίσματος που δέχεται.
Το όρισμα αυτό μπορεί π.χ. να διαβάζεται από κάποιο αρχείο ρυθμίσεων ή να μεταβιβάζεται ως όρισμα γραμμής εντολών στο εξωτερικό πρόγραμμα (στον πελάτη), έτσι ώστε το τελευταίο να είναι κλειστό ως προς το σύνολο των παραγόμενων κλάσεων και να μη χρειάζεται επαναμεταγλώττιση σε περίπτωση που τροποποιηθεί το σύνολο αυτό.
Ένας εναλλακτικός τύπος Factory είναι όταν ορίζεται ως αφηρημένη κλάση και η παρεχόμενη μέθοδος κατασκευής αντικειμένων υποσκελίζεται από υποκλάσεις του, έτσι ώστε η καθεμία από τις τελευταίες να κατασκευάζει αντικείμενο διαφορετικού τύπου.
Σε κάθε περίπτωση στόχος είναι να μπορεί το πρόγραμμα να δημιουργεί στιγμιότυπα κλάσεων χωρίς να προσδιορίζει ρητά τον ακριβή τύπο τους και αφήνοντας το Factory να τον αποφασίσει εσωτερικά· το πρόγραμμα αρκεί να έχει γνώση του γενικού αφηρημένου τύπου.
Υπάρχει ωστόσο και μία εναλλακτική, αρκετά διαφορετική περίπτωση όπου, αντί το Factory να δηλώνεται ως κάποια/ες ξεχωριστή/ες κλάση/εις, αποτελείται απλώς από μία ή περισσότερες στατικές μεθόδους της κλάσης της οποίας πρέπει να κατασκευάζει αντικείμενα (έστω της κλάσης Α).
Τότε ο κατασκευαστής (constructor) της κλάσης δηλώνεται ως ιδιωτική μέθοδος ώστε κάθε απόπειρα του εξωτερικού προγράμματος να δημιουργήσει στιγμιότυπα της Α να γίνεται αναγκαστικά μέσω των μεθόδων Factory οι οποίες επιστρέφουν ένα νέο αντικείμενο τύπου Α.
Σε αυτήν την περίπτωση όμως η Α δεν μπορεί να κληρονομηθεί, αφού για τον σκοπό αυτό πρέπει να υπάρχει τουλάχιστον ένας δημόσιος κατασκευαστής της.
Ακολουθεί ένα απλό παράδειγμα παραμετροποιημένου Factory: interface Logger {
public void log(String msg);
}
class ConsoleLogger implements Logger {
public void log(String msg) { System.err.println("\\nConsole
logging...." + msg + "\\n"); }
}
class FileLogger implements Logger {
public void log(String msg) { System.out.println("\\nFile
logging...." + msg + "\\n"); }
}
public class LoggerFactory {
public Logger getLogger(String choice) { if(choice.equals("FileLogger"))
return new FileLogger(); else if(choice.equals("ConsoleLogger"))
return new ConsoleLogger(); else { System.err.println("\\nError.
Exiting...."); return null; } }
}
public class LogTest {
public static void main(String[] args) { if(args.length
!= 1) { System.out.println("\\nError.
Exiting...."); return; } LoggerFactory fac = new LoggerFactory(); String choice = args[0]; Logger logg = fac.getLogger(choice);
logg.log("Log
This!");
}
} Στον κώδικα αυτόν η κλάση LoggerFactory είναι η μόνη που γνωρίζει τους διαφορετικούς τύπους δεδομένων οι οποίοι υλοποιούν τη διασύνδεση Logger, άρα είναι και το μόνο σημείο του κώδικα που χρειάζεται να τροποποιηθεί σε περίπτωση που η Logger επεκταθεί με ακόμα μία παραγόμενη κλάση που την υλοποιεί.
Το εξωτερικό πρόγραμμα, η κλάση LogTest, χειρίζεται απλώς ένα αντικείμενο του γενικού τύπου Logger και αφήνει στο Factory την απόφαση για τον πραγματικό τύπο του, αναλόγως με ένα όρισμα γραμμής εντολών που δίνεται από τον χρήστη.
Ακολουθεί ένα παράδειγμα ορισμού του Factory ως μεθόδου μίας κλάσης την οποία πρέπει να παράγει: class Complex {
public static Complex fromCartesian(double real, double imag) { return new Complex(real, imag); } public static Complex fromPolar(double modulus, double angle) { return new Complex(modulus * cos(angle), modulus * sin(angle)); } private Complex(double a, double b) {}
}
Complex c = Complex.fromPolar(1,
pi); //Αντί για την τυπική κλήση Complex c = new Complex(....)
η
//οποία θα μπορούσε να γίνει αν ο κατασκευαστής ήταν δημόσιος
Το Abstract Factory είναι ένα πρότυπο που αποτελεί παραλλαγή του Factory και χρησιμοποιείται όταν έχουμε παράλληλες κληρονομικές ιεραρχίες κλάσεων οι οποίες μοιράζονται κάποια κοινή ιδιότητα.
Έστω π.χ. ότι γράφουμε μία βιβλιοθήκη για κατασκευή γραφικών διασυνδέσεων χρήστη (GUI) η οποία περιέχει διάφορα οπτικά στοιχεία (widgets) τα οποία κληρονομούν από μία κοινή διασύνδεση ονόματι Widget (π.χ. κουμπιά, μπάρες κύλισης κλπ).
Υποθέτοντας ότι η βιβλιοθήκη υλοποιείται σε δύο εκδοχές, μία για λειτουργικό σύστημα Windows και μία για Unix, μπορούμε να δηλώσουμε ένα αφηρημένο Factory το οποίο παρέχει μεθόδους για την κατασκευή κάθε πιθανού οπτικού στοιχείου ώστε αυτές οι μέθοδοι να υποσκελίζονται διαφορετικά στις δύο παραγόμενες κλάσεις που υλοποιούν το αφηρημένο Factory: μία για Windows και μία για Unix.
Ακολουθεί ο κώδικας του παραδείγματος:
interface Widget {
....
}
abstract class Button implements Widget {
public abstract void paint(); ....
}
abstract class ScrollBar implements Widget {
public abstract void paint(); ....
}
class UnixButton extends Button {
public void paint() { ....
}
}
class WindowsButton extends Button {
public void paint() { ....
}
}
class UnixScrollBar extends ScrollBar {
public void paint() { ....
}
}
class WindowsScrollBar extends ScrollBar {
public void paint() { ....
}
}
abstract class AbstractGUIFactory {
public abstract Button createButton(); public abstract ScrollBar createScrollBar();
}
class WindowsFactory extends AbstractGUIFactory {
public Button createButton() { return new WindowsButton(); } public ScrollBar createScrollBar() { return new WindowsScrollBar(); }
}
class UnixFactory extends AbstractGUIFactory {
public Button createButton() { return new UnixButton(); } public ScrollBar createScrollBar() { return new UnixScrollBar(); }
}
public class Application {
public static void main(String[] args) { AbstractGUIFactory factory = new UnixFactory(); Button button = factory.createButton();
button.paint();
//Δημιουργείται και σχεδιάζεται ένα αντικείμενο UnixButton ScrollBar scrollbar = factory.createScrollBar();
scrollbar.paint();
//Δημιουργείται και σχεδιάζεται ένα αντικείμενο UnixScrollBar }
}
Όπως είναι φανερό από το παράδειγμα το εξωτερικό πρόγραμμα δε χρειάζεται να γνωρίζει τους ακριβείς τύπους δεδομένων WindowsButton, UnixButton, WindowsScrollBar και UnixScrollBar, καθώς το μόνο που χειρίζεται είναι οι αφηρημένοι τύποι Button και ScrollBar.
Σε περίπτωση που είναι επιθυμητή η αλλαγή λειτουργικού συστήματος η μόνη γραμμή κώδικα που πρέπει να τροποποιηθεί είναι αυτή όπου δημιουργείται το αντικείμενο UnixFactory· αρκεί να γίνει WindowsFactory και τα κατασκευαζόμενα οπτικά στοιχεία θα "προσαρμοστούν" αυτομάτως χωρίς αλλαγές στον υπόλοιπο κώδικα.
Η εναλλακτική λύση είναι να αξιοποιήσουμε δύο συνήθη Factory, όπου το ένα επιστρέφει τύπο Button και γνωρίζει εσωτερικά τους τύπους UnixButton και WindowsButton, ενώ το άλλο επιστρέφει τύπο ScrollBar και γνωρίζει εσωτερικά τους τύπους UnixScrollBar και WindowsScrollBar. Το Abstract Factory αποτελεί ουσιαστικώς μία "συγχώνευση" αυτών των δύο συνηθισμένων Factory, αφού κατασκευάζει τόσο κουμπιά όσο και μπάρες κύλισης, αλλά ταυτόχρονα και "διάσπασή" τους, αφού ορίζονται δύο παραγόμενα Factory: ένα για οπτικά στοιχεία του Unix και ένα για οπτικά στοιχεία των Windows.
Αυτή η διάσπαση όμως δεν είναι απόλυτη αφού όλα τα παραγόμενα Factory υλοποιούν την ίδια διασύνδεση.
300px|thumb|UML: Singleton.
Το Singleton είναι ένα σχεδιαστικό πρότυπο που επιλύει το ζήτημα της εξασφάλισης της ύπαρξης το πολύ ενός στιγμιότυπου κάποιας κλάσης Α κατά το χρόνο εκτέλεσης, ένας περιορισμός που μπορεί να ανακύψει για διάφορους λόγους.
Με το πρότυπο Singleton η αρμοδιότητα για την ικανοποίηση αυτού του περιορισμού ανατίθεται στην ίδια την κλάση Α και δε μεταβιβάζεται στο εξωτερικό πρόγραμμα που τη χρησιμοποιεί.
Αυτό γίνεται μέσω μίας στατικής δημόσιας μεθόδου της Α η οποία δημιουργεί το πρώτο στιγμιότυπο της κλάσης και ακολούθως, σε κάθε επόμενη κλήση της, επιστρέφει έναν δείκτη ή μία αναφορά προς αυτό.
Το Singleton διαφέρει από μία απλή καθολική μεταβλητή (global variable), η οποία επίσης δημιουργείται αναγκαστικά μόνο μία φορά καθ' όλο το χρόνο εκτέλεσης του προγράμματος, καθώς το αντικείμενο Singleton δε δεσμεύει μνήμη μέχρι τη στιγμή της πρώτης κλήσης της μεθόδου κατασκευής.
Από εκεί κι έπειτα όμως το αντικείμενο παραμένει στη μνήμη ως τον τερματισμό του προγράμματος γιατί είναι υλοποιημένο ως στατική μεταβλητή.
Ακολουθεί ένα απλό παράδειγμα:
public class A {
private static A instance; private A() {} //Ο κατασκευαστής ο οποίος μπορεί να κληθεί μόνο από κώδικα της ίδιας της Α public static A getAObject() { if (instance == null) { instance = new A(); } return instance; }
}
Η Α περιέχει ένα στατικό γνώρισμα instance τύπου Α και μία στατική δημόσια μέθοδο getAObject η οποία ελέγχει αν στο γνώρισμα αυτό έχει ανατεθεί τιμή.
Αν ναι επιστρέφει το ίδιο το γνώρισμα, διαφορετικά δημιουργεί ένα νέο στιγμιότυπο της ίδιας της κλάσης Α και το αναθέτει στο πεδίο instance προτού επιστρέψει το τελευταίο.
Ο κατασκευαστής είναι ιδιωτικός ώστε να παρακάμπτεται αναγκαστικώς από το εξωτερικό πρόγραμμα και να χρησιμοποιείται πάντα η μέθοδος getAObject.
// A object = new A(); // ο κατασκευαστής του A είναι ιδιωτικός - δεν επιτρέπεται
// Το αντικείμενο object1 και object2 αναφέρονται στο ίδιο Singleton A object1 = A.getAObject(); A object2 = A.getAObject();
Το Prototype είναι ένα σχεδιαστικό πρότυπο με το οποίο ένα νέο αντικείμενο κατασκευάζεται με "κλωνοποίηση" κάποιου υπάρχοντος.
Η κλωνοποίηση αυτή γίνεται μέσω μίας μεθόδου clone η οποία παρέχεται από μία αφηρημένη κλάση ή διασύνδεση Α και υλοποιείται σε κάθε παραγόμενη κλάση Β η οποία κληρονομεί την Α.
Έτσι η κλήση της clone σε ένα στιγμιότυπο της Β επιστρέφει ένα αντίγραφο του εν λόγω στιγμιότυπου, το οποίο αναλόγως με την υλοποίηση μπορεί να είναι είτε ρηχό, δηλαδή να περιέχει δείκτες προς τις εσωτερικές δομές δεδομένων του αρχικού στιγμιότυπου, είτε βαθύ, δηλαδή να περιέχει πλήρη, νεοδημιουργηθέντα αντίγραφα αυτών των δομών δεδομένων.
Το Prototype χρειάζεται σε περιπτώσεις όπου πρέπει να κατασκευαστεί μία ρέπλικα ενός αντικειμένου αλλά με κάποιον άλλον τρόπο, π.χ. στην Java με χρήση του τελεστή new και ενός κατασκευαστή αντιγράφου (copy constructor), προσβάλλεται η αρχή ανοιχτότητας-κλειστότητας.
Η μέθοδος clone μπορεί να επικαλύπτει την κλήση του εκάστοτε κατασκευαστή αντιγράφου με ένα κοινό επίπεδο αφαίρεσης έτσι ώστε να μη χρειάζεται το εξωτερικό πρόγραμμα να γνωρίζει όλους τους παραγόμενους τύπους δεδομένων που υλοποιούν τη διασύνδεση Α, καθώς η clone επιστρέφει αναφορά του αφηρημένου τύπου Α.
Τα δομικά πρότυπα αφορούν τυποποιημένους τρόπους δυναμικής κατασκευής σύνθετων αντικειμένων τα οποία χρησιμοποιούν υπάρχουσες ιεραρχίες κλάσεων.
Το πρότυπο αυτό επιτρέπει την εύκολη και δυναμική επέκταση της λειτουργικότητας κάποιων υπαρχόντων κλάσεων Α, Β κλπ, οι οποίες υλοποιούν την ίδια διασύνδεση ή κληρονομούν την ίδια αφηρημένη κλάση (έστω Interface), σε χρόνο εκτέλεσης.
Αυτό γίνεται μέσω του Decorator, μίας νέας κλάσης η οποία επίσης υλοποιεί την Interface αλλά περιέχει ως ιδιωτικό πεδίο και μία αναφορά σε ένα στιγμιότυπο του γενικού τύπου Interface (έστω το instance), η οποία τυπικά μεταβιβάζεται ως όρισμα στον κατασκευαστή της Decorator.
Έτσι οι μέθοδοι της τελευταίας υλοποιούν εσωτερικά την καινούργια λειτουργικότητα αλλά για τις κοινές εργασίες καλούν τις αντίστοιχες μεθόδους του instance.
Κατά τον χρόνο εκτέλεσης θα μπορούσε το αντικείμενο Decorator να κατασκευάζεται με όρισμα οποιοδήποτε στιγμιότυπο τύπου Interface (ακόμα και του ίδιου του Decorator, αν και αυτό δε θα είχε ιδιαίτερο νόημα) ώστε κατά περίπτωση το αντικείμενο να παρέχει τη λειτουργικότητα οποιασδήποτε κλάσης τύπου Interface, είτε της Α είτε κάποιας άλλης, επεκτεταμένης με ένα συγκεκριμένο σύνολο δυνατοτήτων.
Με αυτόν τον τρόπο γίνεται εφικτός ένας δυναμικός συνδυασμός λειτουργιών από στοιχειώδεις δομικούς λίθους κατά τον χρόνο εκτέλεσης.
Η εναλλακτική λύση, χωρίς χρήση κάποιου σχεδιαστικού προτύπου, θα ήταν η απλή κληρονομικότητα, με τον ορισμό κλάσεων οι οποίες επεκτείνουν τις Α, Β κλπ.
και προσθέτουν τη νέα λειτουργικότητα.
Ωστόσο η λύση αυτή δεν είναι εφικτή σε περίπτωση που οι Α, Β κλπ.
δεν μπορούν να επεκταθούν με κληρονομικότητα (π.χ. αν δηλωθούν ως τελικές κλάσεις στην Java), ενώ σε άλλες περιπτώσεις δεν είναι καθόλου πρακτική, π.χ. αν έχουμε πολλαπλά διαφορετικά σύνολα νέων δυνατοτήτων τα οποία πρέπει να συνδυαστούν με τις Α, Β κλπ.
Το πρόβλημα έγκειται στο ότι με την κληρονομικότητα όλοι οι πιθανοί συνδυασμοί δυνατοτήτων πρέπει να προβλεφθούν και να ληφθούν υπ' όψιν κατά τη συγγραφή του προγράμματος.
Αντιθέτως με την κλάση Decorator, η οποία δρα ως περίβλημα (wrapper) άλλων αντικειμένων τύπου Interface προς τα οποία περιέχει αναφορές / δείκτες, η σύνθεση νέων αντικειμένων ουσιαστικά γίνεται δυναμικά ενώ το πρόγραμμα εκτελείται.
Ακολουθεί ένα απλό παράδειγμα:
interface Window {
public void draw(); public String getDescription();
}
class SimpleWindow implements Window {
public void draw() { ...
} public String getDescription() { return "simple window"; }
}
abstract class WindowDecorator implements Window {
protected Window decoratedWindow; public WindowDecorator (Window decoratedWindow) { this.decoratedWindow
= decoratedWindow; }
}
class VerticalScrollBarDecorator extends WindowDecorator {
public VerticalScrollBarDecorator (Window decoratedWindow) { super(decoratedWindow); } public void draw() { drawVerticalScrollBar(); decoratedWindow.draw();
} private void drawVerticalScrollBar() { ...
} public String getDescription() { return decoratedWindow.getDescription()
+ ", including vertical scrollbars"; }
}
class HorizontalScrollBarDecorator extends WindowDecorator {
public HorizontalScrollBarDecorator (Window decoratedWindow) { super(decoratedWindow); } public void draw() { drawHorizontalScrollBar(); decoratedWindow.draw();
} private void drawHorizontalScrollBar() { ...
} public String getDescription() { return decoratedWindow.getDescription()
+ ", including horizontal scrollbars"; }
}
public class DecoratedWindowTest {
public static void main(String[] args) { Window decoratedWindow = new HorizontalScrollBarDecorator(new VerticalScrollBarDecorator
(new SimpleWindow()));
System.out.println(decoratedWindow.getDescription());
}
}
Το πρότυπο αυτό επιτρέπει την προσαρμογή της διασύνδεσης που εξάγει μία κλάση Α σε μία πρότυπη διασύνδεση, έστω Interface, που αναμένεται από ένα εξωτερικό πρόγραμμα ώστε να μην παραβιάζεται η αρχή ανοιχτότητας-κλειστότητας.
Αυτό γίνεται μέσω μίας ενδιάμεσης κλάσης, του Adapter, η οποία υλοποιεί το Interface και ταυτοχρόνως είτε κληρονομεί την Α (υλοποίηση με κληρονομικότητα), είτε περιέχει μία ιδιωτική αναφορά σε αντικείμενο τύπου Α (υλοποίηση με συνάθροιση).
Σε κάθε περίπτωση ο Adapter έχει πρόσβαση στις μεθόδους της Α και οι δικές του μέθοδοι, οι υπογραφές των οποίων συμφωνούν με αυτές που αναμένονται από κάποιον πελάτη καθώς προδιαγράφονται από τη διασύνδεση / αφηρημένη κλάση Interface, τις καλούν εσωτερικά και επιστρέφουν τα αποτελέσματα, αποκρύπτοντας παράλληλα τη διαδικασία αυτή από το εκάστοτε πρόγραμμα πελάτη.
Το πρόβλημα που επιλύει το εν λόγω σχεδιαστικό πρότυπο εν γένει προκύπτει όταν μία υπάρχουσα ιεραρχία κλάσεων, οι οποίες υλοποιούν ένα συγκεκριμένο στοιχείο αφαίρεσης, πρέπει να επεκταθεί με την προσθήκη μίας νέας κλάσης η διασύνδεση της οποίας δεν είναι συμβατή με το υπάρχον στοιχείο αφαίρεσης.
Η υλοποίηση του προτύπου Adapter μέσω κληρονομικότητας επιτρέπει την προσαρμογή στη ζητούμενη διασύνδεση και των προστατευμένων μεθόδων της κλάσης Α, αντί μόνο των δημοσίων της, ενώ είναι και ελαφρώς πιο αποδοτική από την υλοποίηση με συνάθροιση, αφού κάθε κλήση στον Adapter "μεταφράζεται" σε κλήση σε μια άλλη μέθοδο του ιδίου αντί για κλήση σε μέθοδο άλλου αντικειμένου.
Από την άλλη η υλοποίηση με συνάθροιση δεν αντιμετωπίζει προβλήματα ακόμα και αν η Α δεν μπορεί να κληρονομηθεί, ενώ επιτρέπει την προσαρμογή όχι μόνο της Α αλλά και κάθε υποκλάσης της λόγω του πολυμορφισμού.
Σε περιπτώσεις που έχουμε ένα στοιχείο αφαίρεσης Α (π.χ. αφηρημένη κλάση ή διασύνδεση) και κάποιες κλάσεις που το υλοποιούν, είναι εύκολη η επέκταση της εν λόγω ιεραρχίας με ακόμα μία παραγόμενη κλάση χωρίς ιδιαίτερο κόπο· αρκεί το εξωτερικό πρόγραμμα να χρησιμοποιεί αναφορές προς τον αφηρημένο τύπο Α και όχι προς τους επιμέρους εξειδικευμένους παραγόμενους τύπους (αρχή ανοιχτότητας-κλειστότητας και αξιοποίηση του πολυμορφισμού).
Όμως δεν είναι εύκολη η επέκταση της ίδιας της διασύνδεσης Α με νέες μεθόδους, ιδιότητες κλπ.,
έστω με ένα καινούργιο στοιχείο αφαίρεσης Β το οποίο κληρονομεί το Α, καθώς οι πιθανές κλάσεις που θα υλοποιούν το Β δεν είναι εύκολο να εκμεταλλευτούν τον υπάρχοντα κώδικα αντίστοιχων κλάσεων οι οποίες υλοποιούν το παλαιό στοιχείο αφαίρεσης Α.
Θα μπορούσε βέβαια αυτό να γίνει με πολλαπλή κληρονομικότητα αλλά η εν λόγω σχεδίαση δεν είναι ευέλικτη και δεν υποστηρίζεται πάντοτε.
Τη λύση δίνει το πρότυπο Bridge, το οποίο χρησιμοποιείται όταν το υπό ανάπτυξη σύστημα περιέχει στοιχεία αφαίρεσης από τα οποία μπορούν να προκύψουν διαφορετικές υλοποιήσεις αλλά και νέα στοιχεία αφαίρεσης, που επίσης μπορούν να υλοποιηθούν σε διάφορες παραγόμενες κλάσεις.
Η εφαρμογή του προτύπου έγκειται στη σχεδίαση δύο ιεραρχιών κληρονομικότητας, μίας ιεραρχίας στοιχείων αφαίρεσης (υψηλού επιπέδου) και μίας ιεραρχίας υλοποιήσεων (χαμηλού επιπέδου).
Οι κλάσεις χαμηλού επιπέδου χρησιμοποιούνται για την οικοδόμηση των κλάσεων υψηλού επιπέδου μέσω συνάθροισης, καθώς το βασικό στοιχείο αφαίρεσης περιέχει μία αναφορά σε γενικού τύπου στιγμιότυπο του βασικού στοιχείου της ιεραρχίας χαμηλού επιπέδου.
Έτσι οποιαδήποτε κλάση της ιεραρχίας υψηλού επιπέδου μπορεί να κάνει χρήση αντικειμένων οποιασδήποτε κλάσης της ιεραρχίας χαμηλού επιπέδου και αλλαγές μπορούν να γίνουν κατά τον χρόνο εκτέλεσης.
Οι μέθοδοι των κλάσεων υψηλού επιπέδου μπορούν να κάνουν χρήση μόνο των μεθόδων του βασικού στοιχείου της ιεραρχίας χαμηλού επιπέδου οι οποίες υποσκελίζονται στις επιμέρους υλοποιήσεις.
Ακολουθεί ένα απλό παράδειγμα, όπου ένα στοιχείο αφαίρεσης Α υλοποιείται με δύο διαφορετικούς τρόπους (μία παραγόμενη κλάση για Windows και μία για Unix).
Σκοπός είναι η επέκταση της Α με ένα στοιχείο αφαίρεσης Β το οποίο κληρονομεί από αυτήν και έχει τις δικές του υλοποιήσεις.
Με αυτόν τον στόχο κατά νου γίνεται χρήση του προτύπου Bridge:
class UnixState //Αντικείμενα αυτής της κλάσης περιέχονται στη μία υλοποίηση (για
//λειτουργικό σύστημα Unix) της αρχικής διασύνδεσης Α
{
public void unixSMethod(){}
};
class UnixState2 //Αντικείμενα αυτής της κλάσης περιέχονται στη μία υλοποίηση (για
//λειτουργικό σύστημα Unix) της επέκτασης της αρχικής διασύνδεσης Α
{
public void unixSMethod2(){}
};
class WindowsState //Αντικείμενα αυτής της κλάσης περιέχονται στη μία υλοποίηση (για
//λειτουργικό σύστημα Windows) της αρχικής διασύνδεσης Α
{
public void windowsSMethod(){}
};
class WindowsState2 //Αντικείμενα αυτής της κλάσης περιέχονται στη μία υλοποίηση (για
//λειτουργικό σύστημα Windows) της επέκτασης της αρχικής διασύνδεσης Α
{
public void windowsSMethod2(){}
};
class PlatformIndependentState //Αντικείμενα αυτής της κλάσης περιέχονται και στις δύο
//υλοποιήσεις της αρχικής διασύνδεσης Α
{
public void piMethod(){}
}
class PlatformIndependentState2 //Αντικείμενα αυτής της κλάσης περιέχονται και στις δύο
//υλοποιήσεις της επέκτασης της αρχικής διασύνδεσης Α
{
public void piMethod2(){}
}
class A //Αυτή η κλάση ουσιαστικά είναι η βασική διασύνδεση στην ιεραρχία υψηλού επιπέδου {
private PlatformIndependentState st; private AImpl impl; //Αυτή η συνάθροιση είναι η "γέφυρα" public A(AImpl anImpl) //Η γέφυρα ορίζεται από το εξωτερικό πρόγραμμα //μέσω της κλήσης του κατασκευαστή με κατάλληλο όρισμα { st = new PlatformIndependentState(); impl = anImpl; } public void doSomething1() { impl.doSomething1Impl();
st.piMethod(); }
};
class AImpl //Αυτή η κλάση ουσιαστικά είναι το βασικό στοιχείο αφαίρεσης της ιεραρχίας
//χαμηλού επιπέδου
{
public AImpl(){} public void doSomething1Impl(){};
};
class B extends A {
private PlatformIndependentState2 st2; public B(AImpl anImpl) { super(anImpl); st2 = new PlatformIndependentState2(); } public void doSomething1() { st2.piMethod2();
super.doSomething1();
}
};
class UnixA extends AImpl {
private UnixState ust; public UnixA() { super(); ust = new UnixState(); } public void doSomething1Impl() { ust.unixSMethod();
}
};
class WindowsA extends AImpl {
private WindowsState wst; public WindowsA() { super(); wst = new WindowsState(); } public void doSomething1Impl() { wst.windowsSMethod();
}
};
class UnixB extends UnixA {
private UnixState2 ust2; public UnixB() { super(); ust2 = new UnixState2(); } public void doSomething1Impl() { ust2.unixSMethod2();
super.doSomething1Impl();
}
};
class WindowsB extends WindowsA {
private WindowsState2 wst2; public WindowsB() { super(); wst = new WindowsState2(); } public void doSomething1Impl() { wst2.windowsSMethod2();
super.doSomething1Impl();
}
};
public class BridgeTest {
public static void main(String args[]) { A anA = new A(new UnixA); B aB = new B(new UnixB); }
};
Σε περίπτωση που, για την εκτέλεση μίας εργασίας, διαθέσιμο είναι ένα περίπλοκο σύνολο αλληλεπιδρώντων κλάσεων (έστω σύνολο Α) οι οποίες συνδυάζονται με διάφορους τρόπους, τότε ο προγραμματιστής μπορεί να αποκρύψει αυτόν τον ιστό αλληλεπιδράσεων "καλύπτοντας" τις επίμαχες μονάδες λογισμικού με μία κλάση η οποία εξάγει μία απλή και εύχρηστη διασύνδεση, ένα σύνολο κατάλληλων δημοσίων μεθόδων το οποίο χρησιμοποιεί εσωτερικά το σύνολο Α.
Η διασύνδεση αυτή μπορεί ακολούθως να αξιοποιηθεί για την παραγωγή νέου κώδικα χωρίς ανάγκη άμεσης καταφυγής στις περίπλοκες κλάσεις του Α, αποτελεί δηλαδή μία "βιτρίνα" υψηλού επιπέδου.
Ένα μη αντικειμενοστρεφές παράδειγμα εφαρμογής του προτύπου Facade είναι ορισμένες συναρτήσεις της πρότυπης βιβλιοθήκης της C οι οποίες αποτελούν απλουστευμένες εκδοχές αντίστοιχων κλήσεων συστήματος του Unix (π.χ. η συνάρτηση fopen επικαλύπτει την κλήση open).
Όταν η πρόσβαση ενός εξωτερικού προγράμματος σε αντικείμενα κάποιας συγκεκριμένης κλάσης (έστω της Α) πρέπει να είναι ελεγχόμενη και να πληροί ορισμένες προϋποθέσεις (π.χ., για λόγους εξοικονόμησης μνήμης, να μη φορτώνεται πραγματικά η Α μέχρι να κληθεί μία μέθοδος της), το πρότυπο Proxy παρέχει έναν τυποποιημένο τρόπο ώστε αυτό να γίνεται διατηρώντας την κλειστότητα του εξωτερικού προγράμματος ως προς την εν λόγω κλάση και τον τρόπο λειτουργίας της· ακόμα και αν η υλοποίηση της αλλάξει ο πελάτης είναι αδιάφορος απέναντι σε αυτές τις αλλαγές (δε χρειάζεται τροποποίηση) χάρη σε ένα επίπεδο αφαίρεσης που του παρέχεται από μία ενδιάμεση κλάση, τον Proxy, η οποία παίζει το ρόλο του μεσολαβητή πρόσβασης.
Ο Proxy είναι που εκτελεί όλους τους απαραίτητους ελέγχους και προσπελαύνει πραγματικά τα αντικείμενα, ενώ ταυτόχρονα υλοποιεί την ίδια διασύνδεση με την Α κι έτσι ο πελάτης, ο οποίος κατέχει μία αναφορά προς στιγμιότυπο του Proxy αντί της Α, δεν αντιλαμβάνεται καν τη μεσολάβησή του· ο Proxy ουσιαστικώς εικονικοποιεί την πρόσβαση στη ζητούμενη κλάση.
Συνήθως κατασκευάζεται, αντί για την Α, από ένα Factory και εντελώς διαφανώς για το εξωτερικό πρόγραμμα, ενώ δεν είναι σπάνιο να περιέχει μία αναφορά στο πραγματικό στιγμιότυπο της Α ως ιδιωτικό πεδίο.
Το σχεδιαστικό αυτό πρότυπο αποτελεί ειδική εφαρμογή της γενικότερης έννοιας του proxy στην πληροφορική.
Με αυτήν τη γενικότερη έννοια ο proxy μπορεί να μεσολαβεί μεταξύ ενός πελάτη και ενός οποιουδήποτε ακριβού, σπάνιου ή περίπλοκου πόρου.
Μία ιδιαίτερη περίπτωση εφαρμογής της έννοιας του proxy αποτελεί ο απομακρυσμένος proxy, όπως τα στελέχη (stubs) πελάτη και διακομιστή στο υπόδειγμα απομακρυσμένης κλήσης διαδικασιών στα συστήματα ενδιάμεσου λογισμικού.
Ακολουθεί ένα παράδειγμα όπου μία κλάση τοπικού Proxy αξιοποιείται έτσι ώστε, αν το επιλέξει ο χρήστης, να καθυστερεί τη φόρτωση στη μνήμη της κλάσης LargeClass μέχρι να κληθεί μία μέθοδός της:
public class LargeClassFactory {
public ILargeClass getLargeClassInstance(String mode, String data) { if(mode.equals("LargeClassProxy"))
return new LargeClassProxy (data); else return new LargeClass (data); }
}
public class LargeClassProxy implements ILargeClass //Ο Proxy υλοποιεί την ίδια διασύνδεση με την
//κλάση LargeClass....
{
private ILargeClass largeClass = null; //....και περιέχει ως πεδίο μία αναφορά σε //στιγμιότυπό της private String title; public LargeClassProxy(String title) { this.title
= title; } public void method1() { System.out.println("Proxy
method 1 has been invoked"); if (largeClass == null) //Αν χρειάζεται δημιουργείται τώρα το στιγμιότυπο //της LargeClass { System.out.println("Large
class instance is created"); largeClass = createLargeClass(); } largeClass.method1();
} public void method2() { System.out.println("Proxy
method 2 has been invoked"); if (largeClass == null) //Αν χρειάζεται δημιουργείται τώρα το στιγμιότυπο //της LargeClass { System.out.println("Large
class instance is created"); largeClass = createLargeClass(); } largeClass.method2();
} private ILargeClass createLargeClass() // Κατασκευή στιγμιοτύπου της LargeClass { ILargeClass lc = null; LargeClassFactory fc = new LargeClassFactory(); lc = fc.getLargeClassInstance("LargeClass", title); return lc; }
};
public class LargeClassTest {
public static void main(String args[]) { LargeClassFactory fc = new LargeClassFactory(); //Καθυστέρηση κατασκευής στιγμιοτύπου ILargeClass lc = fc.getLargeClassInstance("LargeClassProxy", "Title"); //Έτσι επιστρέφεται ο Proxy System.out.println("Doing
other things...."); if(args[0].equals("UseLargeObject"))
//Αν ο χρήστης αποφασίσει να καλέσει μέθοδο....
lc.method1(); //....με την κλήση αυτή ο Proxy κατασκευάζει πραγματικά ένα //αντικείμενο LargeClass else System.out.println("The
Large Object was not used"); }
}
Τα συμπεριφορικά πρότυπα αφορούν τον καταμερισμό αρμοδιοτήτων σε διάφορες κλάσεις και τον ορισμό του τρόπου επικοινωνίας μεταξύ των αντικειμένων τους κατά τον χρόνο εκτέλεσης.
Σε αντίθεση με τα δομικά πρότυπα, τα συμπεριφορικά βρίσκουν εφαρμογή στον αρχικό σχεδιασμό μίας ιεραρχίας κλάσεων και όχι στην εκ των υστέρων επέκταση κάποιας υπάρχουσας ιεραρχίας.
Ο γενικός κανόνας είναι ότι έχουμε να κάνουμε με μία ομάδα κλάσεων οι οποίες υλοποιούν με διαφορετικούς τρόπους μία κοινή διασύνδεση Α· ως συνήθως το εξωτερικό πρόγραμμα είναι προτιμότερο να χειρίζεται μόνον αναφορές του αφηρημένου τύπου Α και να βασίζεται στον πολυμορφισμό ώστε να μην παραβιάζεται η αρχή ανοιχτότητας-κλειστότητας.
Η επικοινωνία μεταξύ αντικειμένων γίνεται με παρόμοιο τρόπο, καθώς όχι σπάνια μία κλήση μεθόδου (ίσως κληση κατασκευαστή) δέχεται ως ορισμα μία αναφορά αφηρημένου τύπου κι έτσι το αντίστοιχο αντικείμενο μπορεί να προσπελαύνει με τυποποιημένο τρόπο δημόσιες μεθόδους στιγμιοτύπων κάθε κλάσης της επίμαχης ιεραρχίας.
Όταν είναι διαθέσιμοι πολλαπλοί τρόποι επίλυσης ενός προβλήματος (π.χ. διαφορετικοί αλγόριθμοι), είναι προτιμότερο ο καθένας από αυτούς να μην υλοποιείται μέσα στις κλάσεις-πελάτες που τον χρησιμοποιούν (π.χ. ως ιδιωτική μέθοδος), έτσι ώστε οι πελάτες να έχουν μικρότερη περιπλοκότητα και οι διάφοροι αλγόριθμοι να είναι επαναχρησιμοποιήσιμοι και προσπελάσιμοι από πολλαπλά εξωτερικά προγράμματα.
Με το πρότυπο Strategy όλοι οι διαφορετικοί αλγόριθμοι ορίζονται ως ξεχωριστές κλάσεις που υλοποιούν μία κοινή διασύνδεση Α και οι πελάτες διατηρούν ως πεδίο μία αναφορά προς τον αφηρημένο τύπο Α.
Στο πεδίο αυτό δίνεται τιμή μέσω του ορίσματος κάποιας μεθόδου του πελάτη (πιθανώς του κατασκευαστή του), έτσι ώστε η ταυτότητα του εκάστοτε χρησιμοποιούμενου αλγορίθμου από όλους τους διαθέσιμους για την εκτέλεση της ζητούμενης εργασίας να είναι εύκολα παραμετροποιήσιμη, με μία απλή αλλαγή αυτού του ορίσματος κατά την κλήση της προαναφερθείσας μεθόδου.
Το πρότυπο αυτό είναι εκλέπτυνση του Strategy για την περίπτωση που κάθε αλγόριθμος έχει πολλαπλές παραλλαγές οι οποίες διαφέρουν σε ορισμένα βήματα αλλά κάποια άλλα σημεία τους είναι κοινά.
Τότε για κάθε αλγόριθμο μπορούμε να ορίσουμε ένα στοιχείο αφαίρεσης Β το οποίο υλοποιεί τη διασύνδεση Α και με τη σειρά του κληρονομείται από πολλές παραλλαγές του αλγορίθμου.
Το κάθε Β περιέχει την υλοποίηση των αμετάβλητων βημάτων και, στα σημεία που οι παραλλαγές διαφέρουν, καλεί αφηρημένες προστατευμένες μεθόδους.
Οι μέθοδοι αυτές υλοποιούνται διαφορετικά σε κάθε παραλλαγή που κληρονομεί το Β.
Το πρότυπο Visitor είναι ένας τρόπος διαχωρισμού κάποιου αλγορίθμου, ο οποίος πιθανώς εκφράζει μία επέκταση στη λειτουργικότητα μίας ιεραρχίας κλάσεων, από τις εν λόγω κλάσεις.
Στόχος είναι η σταδιακή προσθήκη νέων δυνατοτήτων σε όλες τις κλάσεις που υλοποιούν μία διασύνδεση Α χωρίς να χρειάζεται να τροποποιηθούν οι κλάσεις αυτές και χωρίς το εξωτερικό πρόγραμμα να γνωρίζει τους ακριβείς τύπους των αντικειμένων (μπορεί να χειρίζεται τα τελευταία με αφηρημένες αναφορές τύπου Α).
Το μέσον για την επίτευξη του σκοπού αυτού είναι η ομαδοποίηση όλων των μεθόδων που περιγράφουν τη νέα λειτουργικότητα για κάθε κλάση της ιεραρχίας σε μία νέα, ξεχωριστή κλάση Visitor η οποία δεν κληρονομεί από την Α.
Η Visitor περιέχει πολυμορφικές μεθόδους visit η καθεμία από τις οποίες δέχεται διαφορετικού τύπου όρισμα· οι πιθανοί τύποι είναι οι παραγόμενες κλάσεις που υλοποιούν την Α.
Κάθε μέθοδος visit περιγράφει τη νέα λειτουργικότητα που αντιστοιχεί στην κλάση του ορίσματος της (π.χ. μία επεξεργασία των δεδομένων της εν λόγω κλάσης) αλλά οι μέθοδοι αυτές δεν καλούνται άμεσα από τον προγραμματιστή.
Αντιθέτως το εξωτερικό πρόγραμμα, για κάθε αντικείμενο Β της ιεραρχίας στο οποίο θέλει να προσθέσει τη νέα λειτουργικότητα, καλεί μία μέθοδο accept του Β με όρισμα το αντικείμενο Visitor.
Η accept πρέπει να προδιαγράφεται από την Α και να υλοποιείται σε κάθε παραγόμενη κλάση, ενώ το μόνον που κάνει είναι να καλεί με τη σειρά της τη μέθοδο visit του ορίσματος της με όρισμα το αντικείμενο όπου περιέχεται.
Έτσι τελικώς γίνεται η κατάλληλη επεξεργασία, με κλήση της κατάλληλης μεθόδου, χωρίς το εξωτερικό πρόγραμμα να γνωρίζει καν τον ακριβή παραγόμενο τύπο της εκάστοτε Β.
Αν πολλές διαφορετικές επεκτάσεις είναι επιθυμητές, τότε μπορεί η κλάση Visitor να γίνει διασύνδεση και να υλοποιείται με διαφορετικές κλάσεις για κάθε ζητούμενη επέκταση.
right|600px|thumb|UML: Το Σχεδιαστικό Πρότυπο Παρατηρητή / Observer.
Σε περιπτώσεις που κάποια αντικείμενα (παρατηρητές) ενδιαφέρονται να λαμβάνουν ειδοποιήσεις για τυχόν αλλαγές στην κατάσταση κάποιου άλλου αντικειμένου Α (π.χ. τροποποιήσεις τιμών κάποιων πεδίων του), υπάρχουν δύο προσεγγίσεις για την υλοποίηση αυτών των ενημερώσεων: είτε τις κατάλληλες στιγμές το Α να καλεί προκαθορισμένες μεθόδους των παρατηρητών και να τους μεταβιβάζει έτσι δεδομένα, είτε οι παρατηρητές να καλούν μία μέθοδο του Α για να λαμβάνουν κατά βούληση πληροφορίες.
Η ενδιαφέρουσα περίπτωση είναι η πρώτη καθώς μόνον το Α γνωρίζει πότε υπάρχουν αλλαγές στην κατάσταση του και τέτοιες αλλαγές είναι που πρέπει να πυροδοτούν τις ενημερώσεις.
Ο απλούστερος τρόπος είναι να διατηρεί μία συνδεδεμένη λίστα με όλους τους παρατηρητές που κατά καιρούς έχουν εκδηλώσει ενδιαφέρον και να καλεί τις κατάλληλες μεθόδους τους τις κατάλληλες στιγμές.
Όμως αυτή η λύση προκαλεί προβλήματα κλειστότητας καθώς ένας άλλος τύπος παρατηρητή μπορεί να έχει διαφορετικές μεθόδους, ενώ το Α παρουσιάζει υψηλή σύζευξη με άλλες κλάσεις.
Το πρότυπο Observer δίνει μία λύση σε αυτό το πρόβλημα: ορίζει μία διασύνδεση Observable, η οποία περιέχει μία μέθοδο register (για την εκούσια δήλωση παρατηρητών) και μία μέθοδο unregister (για την εκούσια αποχώρηση παρατηρητών), και μία διασύνδεση Observer, με μία μέθοδο synchronize για τη μεταβίβαση ενημερωμένων δεδομένων.
Η κλάση κάθε αντικειμένου Α πρέπει να υλοποιεί τη διασύνδεση Observable και η κλάση κάθε παρατηρητή τη διασύνδεση Observer.
Ένα αντικείμενο Observable διατηρεί μία λίστα με αντικείμενα του αφηρημένου τύπου Observer τα οποία έχουν καλέσει τη μέθοδο register του εν λόγω αντικειμένου.
Οι εγγραφές αυτές χρησιμοποιούνται τις κατάλληλες χρονικές στιγμές για την ειδοποίηση των παρατηρητών και αυτό το είδος αλληλεπίδρασης ονομάζεται publish-subscribe.
Ένα παράδειγμα χρήσης του πρότυπου Objerver είναι ένα κουμπί σε ένα γραφικό περιβάλλον εργασίας.
Χρειαζόμαστε ένα αντικείμενο objserver το οποίο θα παρακολουθεί το κουμπί και όταν αυτό θα πατηθεί να ενεργοποιεί μια εκτέλεση κώδικα.
Στην java τέτοια αντικείμενα λέγονται και listeners.
