Αντικειμενοστρεφής προγραμματισμός

Στην πληροφορική αντικειμενοστρεφή προγραμματισμό (object-oriented programming) ή ΑΠ, ονομάζουμε ένα προγραμματιστικό υπόδειγμα το οποίο εμφανίστηκε στα τέλη της δεκαετίας του 1960 και καθιερώθηκε κατά τη δεκαετία του 1990, αντικαθιστώντας σε μεγάλο βαθμό το παραδοσιακό υπόδειγμα του δομημένου προγραμματισμού. Πρόκειται για μία μεθοδολογία ανάπτυξης προγραμμάτων, υποστηριζόμενη από κατάλληλες γλώσσες προγραμματισμού, όπου ο χειρισμός σχετιζόμενων δεδομένων και των διαδικασιών που επενεργούν σε αυτά γίνεται από κοινού, μέσω μίας δομής δεδομένων που τα περιβάλλει ως αυτόνομη οντότητα με ταυτότητα και δικά της χαρακτηριστικά. Αυτή η δομή δεδομένων καλείται αντικείμενο και αποτελεί πραγματικό στιγμιότυπο στη μνήμη ενός σύνθετου, και πιθανώς οριζόμενου από τον χρήστη, τύπου δεδομένων ονόματι κλάση. Η κλάση προδιαγράφει τόσο δεδομένα όσο και τις διαδικασίες οι οποίες επιδρούν επάνω τους· αυτή υπήρξε η πρωταρχική καινοτομία του ΑΠ.
Έτσι μπορεί να οριστεί μία προδιαγραφή δομής αποθήκευσης (π.χ. μία κλάση «τηλεόραση») η οποία να περιέχει τόσο ιδιότητες (π.χ. μία μεταβλητή «τρέχον κανάλι») όσο και πράξεις ή χειρισμούς επί αυτών των ιδιοτήτων (π.χ. μία διαδικασία «άνοιγμα της τηλεόρασης»). Στο εν λόγω παράδειγμα κάθε υλική τηλεόραση (κάθε αντικείμενο αποθηκευμένο πραγματικά στη μνήμη) αναπαρίσταται ως ξεχωριστό, «φυσικό» στιγμιότυπο αυτής της πρότυπης, ιδεατής κλάσης. Επομένως μόνο τα αντικείμενα καταλαμβάνουν χώρο στη μνήμη του υπολογιστή ενώ οι κλάσεις αποτελούν απλώς «καλούπια». Οι αιτίες που ώθησαν στην ανάπτυξη του ΑΠ ήταν οι ίδιες με αυτές που οδήγησαν στην ανάπτυξη του δομημένου προγραμματισμού (ευκολία συντήρησης, οργάνωσης, χειρισμού και επαναχρησιμοποίησης κώδικα μεγάλων και πολύπλοκων εφαρμογών), όμως τελικώς η αντικειμενοστρέφεια επικράτησε καθώς μπορούσε να αντεπεξέλθει σε προγράμματα πολύ μεγαλύτερου όγκου και πολυπλοκότητας.

Ιστορικό

Οι περισσότερες αντικειμενοστρεφείς έννοιες εμφανίστηκαν αρχικά στη γλώσσα προγραμματισμού Simula 67, η οποία ήταν προσανατολισμένη στην εκτέλεση προσομοιώσεων του πραγματικού κόσμου. Οι ιδέες της Simula 67 επηρέασαν κατά τη δεκαετία του '70 την ανάπτυξη της Smalltalk, της γλώσσας που εισήγαγε τον όρο αντικειμενοστρεφής προγραμματισμός. Η Smalltalk αναπτύχθηκε από τον Άλαν Κέι της εταιρείας Xerox στο πλαίσιο μίας εργασίας με στόχο τη δημιουργία ενός χρήσιμου, αλλά και εύχρηστου, προσωπικού υπολογιστή. Όταν η τελική έκδοση της Smalltalk έγινε διαθέσιμη το 1980 η έρευνα για την αντικατάσταση του δομημένου προγραμματισμού με ένα πιο σύγχρονο υπόδειγμα ήταν ήδη εν εξελίξει. Στη γλώσσα αυτή όλοι οι τύποι δεδομένων ήταν κλάσεις (δεν υπήρχαν δηλαδή πια παραδοσιακές δομές δεδομένων παρά μόνο αντικείμενα).
Την ίδια περίπου εποχή, και επίσης με επιρροές από τη Simula, ολοκληρωνόταν η ανάπτυξη της C++ ως μίας ισχυρής επέκτασης της δημοφιλούς γλώσσας προγραμματισμού C στην οποία είχαν "μεταμοσχευθεί" αντικειμενοστρεφή χαρακτηριστικά. Η επιρροή της C++ καθ' όλη της δεκαετία του '80 ήταν καταλυτική με αποτέλεσμα τη σταδιακή κυκλοφορία αντικειμενοστρεφών εκδόσεων πολλών γνωστών διαδικαστικών γλωσσών προγραμματισμού. Κατά το πρώτο ήμισυ της δεκαετίας του '90 η βαθμιαία καθιέρωση στους μικροϋπολογιστές των γραφικών διασυνδέσεων χρήστη (GUI), για την ανάπτυξη των οποίων ο ΑΠ φαινόταν ιδιαιτέρως κατάλληλος, και η επίδραση της C++ οδήγησαν στην επικράτηση της αντικειμενοστρέφειας ως βασικού προγραμματιστικού υποδείγματος.
Το 1995 η εμφάνιση της Java, μίας ιδιαίτερα επιτυχημένης, πλήρως αντικειμενοστρεφούς γλώσσας που έμοιαζε συντακτικώς με τη C/C++ και προσέφερε πρωτοποριακές για την εποχή δυνατότητες, έδωσε νέα ώθηση στον ΑΠ. Παράλληλα εμφανίστηκαν ποικίλες άτυπες βελτιώσεις στο βασικό προγραμματιστικό υπόδειγμα, όπως οι αντικειμενοστρεφείς γλώσσες μοντελοποίησης λογισμικού, τα σχεδιαστικά πρότυπα κ.λπ. Το 2001 η Microsoft εστίασε την προσοχή της στην πλατφόρμα .NET, μία ανταγωνιστική της Java πλατφόρμα ανάπτυξης και εκτέλεσης λογισμικού, η οποία ήταν εξ ολοκλήρου προσανατολισμένη στην αντικειμενοστρέφεια.

Έννοιες

Κεντρική ιδέα στον αντικειμενοστρεφή προγραμματισμό είναι η κλάση (class), μία αυτοτελής και αφαιρετική αναπαράσταση κάποιας κατηγορίας αντικειμένων, είτε φυσικών αντικειμένων του πραγματικού κόσμου είτε νοητών, εννοιολογικών αντικειμένων, σε ένα περιβάλλον προγραμματισμού. Πρακτικώς είναι ένας τύπος δεδομένων, ή αλλιώς το προσχέδιο μιας δομής δεδομένων με δικά της περιεχόμενα, τόσο μεταβλητές όσο και διαδικασίες. Τα περιεχόμενα αυτά δηλώνονται είτε ως δημόσια (public) είτε ως ιδιωτικά (private), με τα ιδιωτικά να μην είναι προσπελάσιμα από κώδικα εκτός της κλάσης. Οι διαδικασίες των κλάσεων συνήθως καλούνται μέθοδοι (methods) και οι μεταβλητές τους γνωρίσματα (attributes) ή πεδία (fields). Μία κλάση πρέπει ιδανικά να είναι εννοιολογικά αυτοτελής, να περιέχει δηλαδή μόνο πεδία τα οποία περιγράφουν μία κατηγορία αντικειμένων και δημόσιες μεθόδους οι οποίες επενεργούν σε αυτά όταν καλούνται από το εξωτερικό πρόγραμμα, χωρίς να εξαρτώνται από άλλα δεδομένα ή κώδικα εκτός της κλάσης, και επαναχρησιμοποιήσιμη, να αποτελεί δηλαδή μαύρο κουτί δυνάμενο να λειτουργήσει χωρίς τροποποιήσεις ως τμήμα διαφορετικών προγραμμάτων.
Αντικείμενο (object) είναι το στιγμιότυπο μίας κλάσης, δηλαδή αυτή καθαυτή η δομή δεδομένων (με αποκλειστικά δεσμευμένο χώρο στη μνήμη) βασισμένη στο «καλούπι» που προσφέρει η κλάση. Παραδείγματος χάριν, σε μία αντικειμενοστρεφή γλώσσα προγραμματισμού θα μπορούσαμε να ορίσουμε κάποια κλάση ονόματι BankAccount, η οποία αναπαριστά έναν τραπεζικό λογαριασμό, και να δηλώσουμε ένα αντικείμενο της με όνομα MyAccount. Το αντικείμενο αυτό θα έχει δεσμεύσει χώρο στη μνήμη με βάση τις μεταβλητές και τις μεθόδους που περιγράψαμε όταν δηλώσαμε την κλάση. Έτσι, στο αντικείμενο θα μπορούσε να περιέχεται ένα γνώρισμα Balance (=υπόλοιπο) και μία μέθοδος GetBalance (=επίστρεψε το υπόλοιπο). Ακολούθως, θα μπορούσαμε να δημιουργήσουμε ακόμα ένα ή περισσότερα αντικείμενα της ίδιας κλάσης, τα οποία θα είναι διαφορετικές δομές δεδομένων (διαφορετικοί τραπεζικοί λογαριασμοί στο παράδειγμα). Τα αντικείμενα μιας κλάσης μπορούν να προσπελάσουν τα ιδιωτικά περιεχόμενα άλλων αντικειμένων της ίδιας κλάσης.
Ενθυλάκωση δεδομένων (data encapsulation) καλείται η ιδιότητα που προσφέρουν οι κλάσεις να «κρύβουν» τα ιδιωτικά δεδομένα τους από το υπόλοιπο πρόγραμμα και να εξασφαλίζουν πως μόνο μέσω των δημόσιων μεθόδων τους θα μπορούν αυτά να προσπελαστούν. Αυτή η τακτική παρουσιάζει μόνο οφέλη καθώς εξαναγκάζει κάθε εξωτερικό πρόγραμμα να φιλτράρει το χειρισμό που επιθυμεί να κάνει στα πεδία μίας κλάσης μέσω των ελέγχων που μπορούν να περιέχονται στις δημόσιες μεθόδους της κλάσης.
Αφαίρεση δεδομένων καλείται η ιδιότητα των κλάσεων να αναπαριστούν αφαιρετικά πολύπλοκες οντότητες στο προγραμματιστικό περιβάλλον. Μία κλάση αποτελεί ένα αφαιρετικό μοντέλο κάποιας κατηγορίας αντικειμένων. Επίσης, οι κλάσεις προσφέρουν και αφαίρεση ως προς τον υπολογιστή, εφόσον η καθεμία μπορεί να θεωρηθεί ένας μικρός και αυτάρκης υπολογιστής (με δική του κατάσταση, μεθόδους και μεταβλητές).
Κληρονομικότητα ονομάζεται η ιδιότητα των κλάσεων να επεκτείνονται σε νέες κλάσεις, ρητά δηλωμένες ως κληρονόμους (υποκλάσεις ή 'θυγατρικές κλάσεις'), οι οποίες μπορούν να επαναχρησιμοποιήσουν τις μεταβιβάσιμες μεθόδους και ιδιότητες της γονικής τους κλάσης αλλά και να προσθέσουν δικές τους. Στιγμιότυπα των θυγατρικών κλάσεων μπορούν να χρησιμοποιηθούν όπου απαιτούνται στιγμιότυπα των γονικών (εφόσον η θυγατρική είναι κατά κάποιον τρόπο μία πιο εξειδικευμένη εκδοχή της γονικής), αλλά το αντίστροφο δεν ισχύει. Παράδειγμα κληρονομικότητας είναι μία γονική κλάση Vehicle (=Όχημα) και οι δύο πιο εξειδικευμένες υποκλάσεις της Car (=Αυτοκίνητο) και Bicycle (=Ποδήλατο), οι οποίες λέμε ότι "κληρονομούν" από αυτήν. Πολλαπλή κληρονομικότητα είναι η δυνατότητα που προσφέρουν ορισμένες γλώσσες προγραμματισμού μία κλάση να κληρονομεί ταυτόχρονα από περισσότερες από μία γονικές. Από μία υποκλάση μπορούν να προκύψουν νέες υποκλάσεις που κληρονομούν από αυτήν, με αποτέλεσμα μία ιεραρχία κλάσεων που συνδέονται μεταξύ τους "ανά γενιά" με σχέσεις κληρονομικότητας.
Υπερφόρτωση μεθόδου (method overloading) είναι η κατάσταση κατά την οποία υπάρχουν, στην ίδια ή σε διαφορετικές κλάσεις, μέθοδοι με το ίδιο όνομα και πιθανώς διαφορετικά ορίσματα. Αν πρόκειται για μεθόδους της ίδιας κλάσης διαφοροποιούνται μόνο από τις διαφορές τους στα ορίσματα και στον τύπο επιστροφής.
Υποσκέλιση μεθόδου (method overriding) είναι η κατάσταση κατά την οποία μία θυγατρική κλάση και η γονική της έχουν μία μέθοδο ομώνυμη και με τα ίδια ορίσματα. Χάρη στη δυνατότητα του πολυμορφισμού ο μεταγλωττιστής «ξέρει» πότε να καλέσει ποια μέθοδο, βασισμένος στον τύπο του τρέχοντος αντικειμένου. Δηλαδή πολυμορφισμός είναι η δυνατότητα των αντικειμενοστρεφών μεταγλωττιστών να αποφασίζουν δυναμικά ποια είναι η κατάλληλη να κληθεί μέθοδος σε συνθήκες υποσκέλισης.
Αφηρημένη κλάση (abstract class) είναι μία κλάση που ορίζεται μόνο για να κληρονομηθεί σε θυγατρικές υποκλάσεις και δεν υπάρχουν δικά της στιγμιότυπα (αντικείμενα). Η αφηρημένη κλάση ορίζει απλώς ένα "συμβόλαιο" το οποίο θα πρέπει να ακολουθούν οι υποκλάσεις της όσον αφορά τις υπογραφές των μεθόδων τους (όπου ως υπογραφή ορίζεται το όνομα, τα ορίσματα και η τιμή επιστροφής μίας διαδικασίας). Μία αφηρημένη κλάση μπορεί να έχει και μη αφηρημένες μεθόδους οι οποίες υλοποιούνται στην ίδια την κλάση (αν και φυσικά μπορούν να υποσκελίζονται σε υποκλάσεις). Αντιθέτως, οι αφηρημένες μέθοδοί της είναι απλώς ένας ορισμός της υπογραφής τους και εναπόκειται στις υποκλάσεις να τις υλοποιήσουν. Μία αφηρημένη κλάση που δεν έχει γνωρίσματα και όλες οι μέθοδοί της είναι αφηρημένες και δημόσιες καλείται διασύνδεση (interface). Οι κλάσεις που κληρονομούν από μία διασύνδεση λέγεται ότι την "υλοποιούν".
Ακολουθεί ένα απλό παράδειγμα σε γλώσσα προγραμματισμού Java:
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 LogTest{
    public static void main(String[] args)
    {
       if(args.length != 1)
       {
           System.out.println("\nError. Exiting...");
           return;
       }
       Logger logg;
       String choice = args[0];
       if(choice.equals("FileLogger"))
          logg = new FileLogger();
       else if(choice.equals("ConsoleLogger"))
          logg = new ConsoleLogger();
       else
       {
          System.err.println("\nError. Exiting...");
          return;
       }
       logg.log("Log This!");
    }}
Στο παραπάνω παράδειγμα ορίζουμε μία διασύνδεση Logger η οποία παρέχει την υπογραφή μίας μεθόδου log, που υποθέτουμε πως πρέπει να καταγράφει κάπου πληροφορίες για τα σφάλματα που συναντά η εφαρμογή όταν εκτελείται (οι πληροφορίες αυτές τής μεταβιβάζονται με το αλφαριθμητικό όρισμα msg). Η κλάση ConsoleLogger και η κλάση FileLogger είναι δύο διαφορετικές κλάσεις που υλοποιούν τη διασύνδεση Logger και υποσκελίζουν, η καθεμία με διαφορετικό τρόπο, τη μέθοδο log ώστε η μία να καταγράφει πληροφορίες στην οθόνη και η άλλη σε κάποιο αρχείο.
Το πρόγραμμα LogTest είναι ένα μικρό δοκιμαστικό πρόγραμμα το οποίο δέχεται ως όρισμα γραμμής εντολών το πού επιθυμεί ο χρήστης να γίνεται η καταγραφή και δημιουργεί ένα στιγμιότυπο της αντίστοιχης κλάσης: της ConsoleLogger ή της FileLogger. Το στιγμιότυπο αυτό δηλώνεται με τον γενικότερο τύπο Logger, τον τύπο δηλαδή της διασύνδεσης που υλοποιούν και οι δύο κλάσεις, αλλά χάρη στον πολυμορφισμό καλείται αυτομάτως η κατάλληλη εκδοχή της μεθόδου log.

Μετρικές ποιότητας

Κατά καιρούς έχουν οριστεί κάποιες μετρικές για την εκτίμηση της περιπλοκότητας και της ποιότητας μίας αντικειμενοστρεφούς σχεδίασης λογισμικού. Σε αυτές συμπεριλαμβάνονται και οι μετρικές έλλειψης συνοχής (LCOM). Ακολουθεί μία σύνοψη των σπουδαιότερων:

Weighted Methods per Class (WMC)

Το ακέραιο πλήθος των μεθόδων που ορίζονται σε μία κλάση. Δεν υπάρχει ένα καθιερωμένο εύρος βέλτιστου WMC αλλά είναι γενικώς αποδεκτό ότι υπερβολικά μεγάλο WMC οδηγεί σε μεγαλύτερη πιθανότητα σφαλμάτων και προβλημάτων συντήρησης.

Depth of Inheritance Tree (DIT)

Το μήκος του μέγιστου μονοπατιού κληρονομικότητας από την τρέχουσα κλάση έως τη ρίζα μιας ιεραρχίας κλάσεων. Μεγάλες τιμές DIT αυξάνουν την πολυπλοκότητα της σχεδίασης, την πιθανότητα σφαλμάτων αλλά και την επαναχρησιμοποίηση κώδικα λόγω της κληροδότησης μεθόδων. Μία βέλτιστη τιμή DIT θεωρείται το 5.

Number of Children (NOC)

Το πλήθος των κλάσεων που κληρονομούν άμεσα από την τρέχουσα. Υψηλό NOC σημαίνει υψηλή επαναχρησιμοποίηση κώδικα αλλά πιθανόν να υποδεικνύει εννοιολογικά εσφαλμένη χρήση της κληρονομικότητας. Δεν υπάρχει κάποιο γενικώς αποδεκτό βέλτιστο NOC, αφού αυτό εξαρτάται από την εκάστοτε κλάση, αλλά εν γένει οι κλάσεις οι ευρισκόμενες υψηλότερα στην ιεραρχία κληρονομικότητας είναι θεμιτό να έχουν υψηλότερο NOC από όσες τοποθετούνται χαμηλότερα αφού είναι λιγότερο εξειδικευμένες.

Coupling Between Object Classes (CBO)

Το πλήθος των κλάσεων από τις οποίες εξαρτάται η τρέχουσα κλάση. Κάθε τέτοια εξάρτηση μπορεί να είναι αμφίδρομη ή μονόδρομη οποιασδήποτε κατεύθυνσης. Επομένως υψηλό CBO σημαίνει υψηλή σύζευξη (coupling) και δεν είναι επιθυμητό, καθώς αποτρέπει την επαναχρησιμοποίηση κώδικα και ζημιώνει τον αρθρωτό σχεδιασμό του προγράμματος. Όσο χαμηλότερο είναι το CBO μίας κλάσης τόσο πιθανότερο είναι να μπορεί η τελευταία να επαναχρησιμοποιηθεί ως μαύρο κουτί. Υπερβολικά υψηλές θεωρούνται οι τιμές CBO > 14.

Response for a Class (RFC και RFC’)

Η μετρική αυτή ισούται με το πλήθος των μεθόδων που μπορεί να εκτελεστούν ως απάντηση στη λήψη ενός μηνύματος / συμβάντος από την τρέχουσα κλάση (έστω Α), τόσο τοπικών της μεθόδων όσο και «απομακρυσμένων» μεθόδων άλλων κλάσεων οι οποίες καλούνται άμεσα από την Α. Η μετρική RFC’ συμπεριλαμβάνει επιπλέον και τις απομακρυσμένες μεθόδους που καλούνται εμμέσως (π. χ. από άμεσα καλούμενες απομακρυσμένες μεθόδους ή, αναδρομικά, από άλλες έμμεσα καλούμενες απομακρυσμένες μεθόδους). Κάθε μέθοδος μετράται μόνο μία φορά στον υπολογισμό του RFC, ανεξαρτήτως του πόσες φορές καλείται. Υψηλή τιμή RFC υποδεικνύει υψηλές πιθανότητες σφάλματος και δυσκολία συντήρησης.

LCOM1

Μετρική έλλειψης συνοχής. Ισχύει LCOM1 = P - Q αν P > Q, διαφορετικά LCOM1 = 0. Τα P, Q υπολογίζονται με τον ακόλουθο αλγόριθμο (σε ψευδοκώδικα):
  P = 0; Q = 0;
  Για κάθε ζεύγος μεθόδων της κλάσης
  do
  {
     Αν τα σύνολα των πεδίων που χρησιμοποιούν οι δύο τρέχουσες μέθοδοι είναι ξένα μεταξύ τους
        P = P + 1;
     Διαφορετικά
        Q = Q + 1;
  }
Μία τιμή LCOM1 = 0 υποδεικνύει συνεκτική κλάση, ενώ αν LCOM1 > 0 η κλάση καλό είναι να διασπαστεί. Η μετρική LCOM1 παρουσιάζει κάποια προβλήματα, όπως π.χ. ότι δίνει τιμή 0 για κλάσεις πολύ διαφορετικές μεταξύ τους.

LCOM2

Βελτιωμένη μετρική έλλειψης συνοχής. Ισχύει LCOM2 = 1 - sum(mA)/(m*a), όπου m το πλήθος των μεθόδων της κλάσης, a το πλήθος των πεδίων της, mA το πλήθος των μεθόδων που προσπελαύνουν ένα γνώρισμα και sum(mA) το άθροισμα των mA για όλα τα πεδία μίας κλάσης. Η μετρική LCOM2 αποτελεί τον μέσο όρο των ποσοστών των μεθόδων που δε χρησιμοποιούν κάθε γνώρισμα. LCOM2 = 0 (υψηλή συνοχή) σημαίνει πως όλες οι μέθοδοι της κλάσης χρησιμοποιούν όλα τα πεδία της, ενώ LCOM2 = 1 (καμία συνοχή) σημαίνει πως καμία μέθοδος δεν προσπελαύνει κανένα πεδίο.

LCOM3

Εναλλακτική μετρική έλλειψης συνοχής. Ισχύει LCOM3 = (m - sum(mA)/a) / (m-1). Η μετρική LCOM3 λαμβάνει τιμές από 0 (υψηλή συνοχή) έως 2 (καμία συνοχή). Τιμές μεγαλύτερες του 1 σημαίνουν πως με βεβαιότητα υπάρχει τουλάχιστον ένα "νεκρό γνώρισμα", δηλαδή γνώρισμα το οποίο δεν προσπελαύνεται από καμία μέθοδο της κλάσης.