ΟΙΚΟΝΟΜΙΚΟ ΠΑΝΕΠΙΣΤΗΜΙΟ ΑΘΗΝΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ ΜΕΤΑΠΤΥΧΙΑΚΟ ΔΙΠΛΩΜΑ ΕΙΔΙΚΕΥΣΗΣ (MSc) στα ΠΛΗΡΟΦΟΡΙΑΚΑ ΣΥΣΤΗΜΑΤΑ ΔΙΠΛΩΜΑΤΙKH ΕΡΓΑΣΙΑ «Αναδόμηση Κώδικα με Πρότυπα Σχεδίασης» Γαϊτάνη Μαρία-Άννα ΜΜ4110004 ΑΘΗΝΑ, ΣΕΠΤΕΜΒΡΙΟΣ 2013
ΜΕΤΑΠΤΥΧΙΑΚΟ ΔΙΠΛΩΜΑ ΕΙΔΙΚΕΥΣΗΣ (MSc) στα ΠΛΗΡΟΦΟΡΙΑΚΑ ΣΥΣΤΗΜΑΤΑ ΔΙΠΛΩΜΑΤΙKH ΕΡΓΑΣΙΑ «Αναδόμηση Κώδικα με Πρότυπα Σχεδίασης» Γαϊτάνη Μαρία-Άννα ΜΜ4110004 Επιβλέπων Καθηγητής: Γιακουμάκης Εμμανουήλ Εξωτερικός Κριτής: Μαλεύρης Νικόλαος ΟΙΚΟΝΟΜΙΚΟ ΠΑΝΕΠΙΣΤΗΜΙΟ ΑΘΗΝΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ ΑΘΗΝΑ, ΣΕΠΤΕΜΒΡΙΟΣ 2013
Ευχαριστίες Θα ήθελα να ευχαριστήσω όλους τους ανθρώπους οι οποίοι συνέβαλαν καθοριστικά στην ολοκλήρωση αυτής της διπλωματικής εργασίας. Καταρχάς, θα ήθελα να εκφράσω την ευγνωμοσύνη μου στον επιβλέποντα καθηγητή μου κ. Εμμανουήλ Γιακουμάκη για την εμπιστοσύνη που μου έδειξε και για την ευκαιρία που μου έδωσε να ασχοληθώ με ένα τόσο ενδιαφέρον ερευνητικό θέμα. Επίσης, θερμά τον ευχαριστώ για την καθοδήγηση που μου παρείχε καθ όλη τη διάρκεια της εκπόνησης της διπλωματικής μου εργασίας. Επιπλέον, θα ήθελα να ευχαριστήσω θερμά τον διδάκτορα Πληροφορικής κ. Νικόλαο Διαμαντίδη και τον διδάκτορα Πληροφορικής κ. Βασίλη Ζαφείρη γιατί χωρίς την πολύτιμη βοήθειά τους δε θα ήταν δυνατή η ολοκλήρωση της διπλωματικής μου εργασίας. Ιδιαιτέρως τους ευχαριστώ για την άψογη συνεργασία μας και για τον πολύτιμο χρόνο που διέθεσαν για να με υποστηρίξουν και καθοδηγήσουν καθ όλη τη διάρκεια της εκπόνησης της εργασίας μου. Και οι δυο τους με τις γνώσεις τους, τις ιδέες τους, τις χρήσιμες συμβουλές τους και με περίσσεια υπομονή στάθηκαν αρωγοί μου καθ όλη τη διάρκεια της ερευνητικής μου μελέτης και της συγγραφής της διπλωματικής μου εργασίας. Αφιερώνω τη διπλωματική μου εργασία στους γονείς μου Γιώργο και Ελένη και στον αδερφό μου Παναγιώτη, που πάντα στέκονται στο πλευρό μου και με στηρίζουν σε όλες μου τις επιλογές, και στον αρραβωνιαστικό μου Γιάννη, για την αμέριστη συμπαράσταση, βοήθεια και υπομονή που επέδειξε καθ όλη τη διάρκεια των μεταπτυχιακών μου σπουδών και με αυτόν τον τρόπο βοήθησε να γίνει πιο εύκολο και ευχάριστο αυτό το δύσκολο έργο. 4
Περίληψη Η ανάγκη διασφάλισης της μακροβιότητας και της ανταγωνιστικότητας των σύγχρονων συστημάτων λογισμικού έχει οδηγήσει στην υιοθέτηση της μεθοδολογίας της εξελικτικής ανάπτυξης λογισμικού, σύμφωνα με την οποία το λογισμικό εξελίσσεται καθ όλη τη διάρκεια του κύκλου ζωής του, ακόμη και μετά την παράδοσή του. Ωστόσο, οι συνεχείς τροποποιήσεις στις οποίες υπόκειται ένα σύστημα λογισμικού δύνανται να οδηγήσουν σε βαθμιαία υποβάθμιση της ποιότητας της σχεδίασής του. Προς την κατεύθυνση της βελτίωσης της ποιότητας σχεδίασης του λογισμικού στοχεύει η μεθοδολογία της αναδόμησης με πρότυπα σχεδίασης, η οποία και αποτελεί κύρια πρακτική της εξελικτικής σχεδίασης. Η αναδόμηση είναι η διαδικασία εφαρμογής μικρών και τυποποιημένων μετασχηματισμών στην εσωτερική δομή του λογισμικού, με σκοπό τη βελτίωση ορισμένων ποιοτικών χαρακτηριστικών του και με τρόπο που να μη μεταβάλλεται η εξωτερικά παρατηρήσιμη συμπεριφορά του. Τα πρότυπα σχεδίασης, από την άλλη, αποτελούν ευρέως αποδεκτές, έτοιμες ή σχεδόν έτοιμες υψηλού επιπέδου σχεδιαστικές λύσεις σε κοινά επαναλαμβανόμενα προβλήματα σχεδίασης. Η πρακτική της αναδόμησης με πρότυπα σχεδίασης επιχειρεί να συνδυάσει τα οφέλη από την εφαρμογή μιας αναδόμησης με τα πλεονεκτήματα των προτύπων σχεδίασης, με απώτερο σκοπό τη βελτίωση της ποιότητας σχεδίασης ενός συστήματος λογισμικού. Ωστόσο, η αναδόμηση είναι μια ιδιαίτερα απαιτητική διαδικασία, τόσο αναφορικά με την απαιτούμενη προσπάθεια, όσο και σχετικά με το χρόνο που πρέπει να δαπανηθεί ώστε να πραγματοποιηθεί με ασφάλεια, χωρίς την εισαγωγή σφαλμάτων και διασφαλίζοντας τη διατήρηση της αρχικής συμπεριφοράς του λογισμικού. Οι απαιτήσεις αυτές σε συνδυασμό με το διαρκώς αυξανόμενο μέγεθος των σύγχρονων συστημάτων λογισμικού καθιστούν δύσκολη τη χειρωνακτική διενέργειά της από τους μηχανικούς λογισμικού. Η αξία της αυτοματοποίησης της διαδικασίας της αναδόμησης έχει επιβεβαιωθεί από τη βιομηχανία λογισμικού και πληθώρα εργαλείων αυτόματης αναδόμησης έχει 5
αναπτυχθεί. Ωστόσο, η πλειοψηφία των εργαλείων αυτόματης αναδόμησης υποστηρίζει μόνο απλές αναδομήσεις χαμηλού επιπέδου, χωρίς να παρέχει τη δυνατότητα αυτοματοποιημένης εκτέλεσης πιο σύνθετων αναδομήσεων, όπως οι αναδομήσεις που αποσκοπούν στην εισαγωγή κάποιου σχεδιαστικού προτύπου. Την έλλειψη αυτή προτίθεται να μειώσει η παρούσα διπλωματική εργασία, η οποία και μελετά το πρόβλημα της αυτοματοποίησης της αναδόμησης προς το σχεδιαστικό πρότυπο Null Αντικείμενο (Null Object). Το πρότυπο Null Object, βασιζόμενο στις αρχές της κληρονομικότητας και του πολυμορφισμού, ενδείκνυται να χρησιμοποιηθεί στις περιπτώσεις όπου υπάρχουν στον κώδικα επαναλαμβανόμενοι έλεγχοι αν ένα αντικείμενο είναι διάφορο του null προτού σταλεί ένα μήνυμα σε αυτό. Στόχος της μεθοδολογίας που αναπτύχθηκε είναι η αυτοματοποιημένη ανίχνευση περιπτώσεων που είναι δυνατή η εφαρμογή του προτύπου Null Object. Επιπλέον, η προτεινόμενη προσέγγιση επιτρέπει, για τις περιπτώσεις που πληρούν τα κριτήρια αυτόματης εφαρμογής της αναδόμησης, και την αυτοματοποιημένη ανασύνταξη του πηγαίου κώδικα ώστε να εφαρμοστεί η δομή του προτύπου. Πιο συγκεκριμένα, η προτεινόμενη μεθοδολογία περιορίζεται στην ανίχνευση και απομάκρυνση υποθετικής λογικής που ελέγχει αν ένα πεδίο είναι ίσο ή όχι με null προτού καλέσει κάποια μέθοδο αυτού, ώστε να αποφευχθεί η πρόκληση Null Pointer Exception, μιας και o προγραμματιστής δεν είναι σίγουρος αν στο πεδίο έχει τεθεί τιμή διάφορη του null. Προκειμένου να οριοθετηθεί το πρόβλημα της ανίχνευσης των περιπτώσεων αναδόμησης προς το πρότυπο Null Object, η προτεινόμενη μεθοδολογία ανίχνευσης, αρχικά προσπαθεί να εντοπίσει σε κάθε κλάση του υπό εξέταση κώδικα εκείνα τα πεδία της που είναι τύπου κάποιας κλάσης του υπό εξέταση συστήματος, ώστε να είναι δυνατή η εφαρμογή της αναδόμησης. Τα πεδία αυτά στη συνέχεια εξετάζονται για να εξακριβωθεί αν ικανοποιούν ένα σύνολο επιλεγμένων κριτηρίων που υποδηλώνουν ότι υπάρχει μεγάλη πιθανότητα στην κλάση όπου ορίζονται να υπάρχουν υποθετικές εκφράσεις που ελέγχουν αν η τιμή τους ισούται ή όχι με null. Κάθε πεδίο που ικανοποιεί τα κριτήρια της προηγούμενης ανάλυσης χαρακτηρίζεται ως υποψήφιο για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Στη συνέχεια εξετάζεται κατά πόσο υπάρχουν υποθετικές δηλώσεις που ελέγχουν αν το 6
πεδίο είναι ίσο ή διάφορο του null και οι οποίες πληρούν ένα σύνολο κριτηρίων που καθιστούν δυνατή τη διαγραφή τους, εξασφαλίζοντας παράλληλα τη μη εισαγωγή σφαλμάτων και τη διατήρηση της συμπεριφοράς του λογισμικού. Μόνο τα πεδία για τα οποία θα ανιχνευτούν υποθετικές δηλώσεις υποψήφιες για διαγραφή θα θεωρηθούν τελικά κατάλληλα για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object, μιας και μόνο τότε θα έχουμε όφελος από τη χρήση του προτύπου. Επιπλέον, ένα τρίτο σύνολο κριτηρίων εξετάζεται ώστε να αποφανθούμε αν για τα κατάλληλα πεδία δύναται να αυτοματοποιηθεί η εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Σε διαφορετική περίπτωση, η προτεινόμενη μεθοδολογία θα κατατάσσει αυτό το πεδίο ως suggested refactorable, μιας και απαιτείται η επέμβαση του προγραμματιστή για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Αναφορικά με την υλοποίηση της αυτόματης ανίχνευσης υποψήφιων περιπτώσεων αναδόμησης προς το πρότυπο Null Object, η προτεινόμενη μεθοδολογία βασίστηκε στην τεχνική της στατικής ανάλυσης (static analysis) του πηγαίου κώδικα, ώστε να παραχθεί ως κύρια μορφή αναπαράστασής του το Αφηρημένο Συντακτικό Δέντρο (Abstract Syntax Tree, AST). Το AST δέντρο αναλύεται ώστε να εντοπιστούν τα πεδία και οι υποθετικές δομές που ικανοποιούν τις προϋποθέσεις που ορίζονται από τη μεθοδολογία, ενώ για ένα συγκεκριμένο υποσύνολο των κριτηρίων ανίχνευσης, που οι απαιτούμενες πληροφορίες δεν ήταν δυνατό να εξαχθούν από αυτό, χρησιμοποιήθηκε τόσο ο Γράφος Ροής Ελέγχου (Control Flow Graph, CFG) όσο και ο Γράφος Εξαρτήσεων Προγράμματος (Program Dependence Graph, PDG). Οι προτάσεις αναδόμησης που προκύπτουν ως αποτέλεσμα της διαδικασίας ανίχνευσης και εμφανίζονται στον χρήστη, αντιστοιχούν στις υποθετικές δηλώσεις που η έκφρασή τους ελέγχει την τιμή κάποιου υποψήφιου για αναδόμηση πεδίου. Οι προτάσεις αυτές εμφανίζονται στο χρήστη ταξινομημένες ανάλογα με το αν μπορούν να διαγραφούν και ομαδοποιημένες ανά υποψήφιο πεδίο. Ακολούθως, ο χρήστης δύναται να επιθεωρήσει τις προτάσεις αναδόμησης μία προς μία και να επιλέξει εκείνες που επιθυμεί να εκτελέσει. Κατά την εφαρμογή της επιλεγμένης από το χρήστη πρότασης αναδόμησης, το αφηρημένο συντακτικό δέντρο του υπό εξέταση κώδικα ανασυντάσσεται και 7
παρέχεται στο χρήστη η δυνατότητα προεπισκόπησης της προτεινόμενης αναδόμησης προτού επιλέξει την εφαρμογή της, καθώς και η δυνατότητα απόρριψής της. Όταν ο χρήστης επιβεβαιώσει την εφαρμογή της προτεινόμενης αναδόμησης, οι μετασχηματισμοί του AST δέντρου χρησιμοποιούνται ώστε να τροποποιηθεί ο πηγαίος κώδικας του λογισμικού χωρίς την παρέμβαση του χρήστη. Κατά την εφαρμογή μιας αναδόμησης με χρήση του προτύπου Null Object, οι μετασχηματισμοί που εφαρμόζονται στο AST δέντρο αποσκοπούν στην υλοποίηση των δομικών στοιχείων του προτύπου στο αναδομούμενο σύστημα, καθώς και στην εξασφάλιση της διατήρησης της αρχικής συμπεριφοράς του υπό εξέταση συστήματος. Τα δομικά στοιχεία από τα οποία αποτελείται το πρότυπο Null Object, όταν ακολουθείται η προσέγγιση δημιουργίας αφηρημένης υπερκλάσης, είναι η κλάση πελάτης που δηλώνει το υποψήφιο πεδίο, το πεδίο για το οποίο διαγράφονται οι υποθετικοί έλεγχοι, η αφηρημένη κλάση και οι συγκεκριμένες υποκλάσεις της. Κατά συνέπεια, οι εκτελούμενοι μετασχηματισμοί της αναδόμησης, συνοψίζονται στην δημιουργία της αφηρημένης κλάσης και της συγκεκριμένης Null Object υποκλάσης της, οι μέθοδοι της οποίας θα έχουν την κατάλληλη συμπεριφορά ώστε να είναι δυνατή η διαγραφή όσο το δυνατόν περισσότερων υποθετικών δηλώσεων από το πρόγραμμα. Επιπλέον, το επιλεγμένο πεδίο θα πρέπει να δηλωθεί με τύπο την αφηρημένη υπερκλάση και να αρχικοποιηθεί με ένα στιγμιότυπο της Null Object υποκλάσης. Τέλος, η κλάση πελάτης πρέπει να τροποποιηθεί καταλλήλως, ώστε να διαγραφούν οι υποθετικές δηλώσεις που δύναται να διαγραφούν, αλλά και να διατηρηθεί ίδια η συμπεριφορά του λογισμικού. Η προτεινόμενη μεθοδολογία υλοποιήθηκε με χρήση της γλώσσας προγραμματισμού Java και των βιβλιοθηκών JDT και LTK του Eclipse και ενσωματώθηκε σαν τμήμα του υπάρχοντος εργαλείου αναδόμησης του Eclipse IDE, JDeodorant. Το εργαλείο JDeodorant, στο οποίο και στηρίχτηκε η υλοποίηση της προτεινόμενης μεθοδολογίας, είναι ένα plug-in του Eclipse που παρέχει τη δυνατότητα εκτέλεσης αναδομήσεων που επιλύουν τα σχεδιαστικά προβλήματα «God Class», «Long Method», «Type Checking» και «Feature Envy». Μετά την ολοκλήρωση της υλοποίησης του σχετικού υποστηρικτικού εργαλείου, η προτεινόμενη μεθοδολογία δοκιμάστηκε σε διάφορα συστήματα λογισμικού ανοιχτού κώδικα, προκειμένου να αξιολογηθεί η αποτελεσματικότητα της τόσο αναφορικά με 8
τη χρησιμότητα και την εφαρμοσιμότητά της στην πράξη όσο και ως προς την κλιμακωσιμότητά της. Ιδιαίτερη μέριμνα δόθηκε ώστε τα συστήματα λογισμικού που χρησιμοποιήθηκαν κατά την πειραματική αξιολόγηση να είναι αρκετά σε πλήθος και μεγάλα από άποψη μεγέθους και συνθετότητας. Η απόφαση αυτή πάρθηκε για λόγους πληρότητας, ώστε η χρησιμότητα αλλά και η κλιμακωσιμότητα της μεθοδολογίας να εξεταστεί σε διάφορα συστήματα λογισμικού που διαφέρουν ως προς τα χαρακτηριστικά μεγέθους και πολυπλοκότητας και αντιπροσωπεύουν συστήματα λογισμικού που χρησιμοποιούνται στην πράξη. Στα πλαίσια της αξιολόγησης της χρησιμότητας και της εφαρμοσιμότητας στην πράξη της προτεινόμενης μεθοδολογίας, για καθένα από τα υπό εξέταση συστήματα λογισμικού καταγράφηκαν τόσο οι περιπτώσεις πεδίων που ανιχνεύονται ως κατάλληλες για αυτοματοποιημένη εφαρμογή της αναδόμησης, όσο και τα suggested refactorable πεδία. Όσον αφορά την αξιολόγηση της κλιμακωσιμότητας της προτεινόμενης μεθοδολογίας, κατεγράφησαν για καθένα σύστημα λογισμικού οι χρόνοι εκτέλεσης της διαδικασίας ανίχνευσης των προτάσεων αναδόμησης. Τα αποτελέσματα των προαναφερθέντων πειραμάτων παρουσιάζονται και χρησιμοποιούνται προκειμένου να εξαχθούν ορισμένα χρήσιμα συμπεράσματα. Τέλος, η παρούσα διπλωματική εργασία καταγράφει τους περιορισμούς και τις ελλείψεις που παρουσιάζει η προτεινόμενη προσέγγιση αναδόμησης προς το πρότυπο Null Object. Οι περιορισμοί αυτοί μπορούν να γίνουν αντικείμενο μελλοντικής ερευνητικής μελέτης, ώστε να επεκταθεί και εμπλουτιστεί η προτεινόμενη μεθοδολογία. 9
Abstract The need to ensure the longevity and competitiveness of modern software systems has led to the adoption of the evolutionary software development methodology, according to which the software evolves throughout its life cycle, even past its release. However, the continuous modifications to which a software system is subject, can lead to a gradual deterioration of its design quality. The methodology of refactoring to patterns, which is one of the key principles of evolutionary design, aims towards improving the design quality of software. Refactoring is the process of applying small and formalized transformations to the internal structure of the software, in order to improve some of its quality characteristics, without changing its externally observable behavior. Design patterns, on the other hand, are widely accepted, ready or almost ready high-level design solutions to commonly occurring design problems. The technique of refactoring to patterns attempts to combine the benefits of refactoring with the advantages of design patterns, with the ultimate aim of improving the design quality of a software system. However, refactoring is a highly demanding process, both in terms of effort required, and of time that should be spent in order to be performed safely, without introducing errors and by guaranteeing the preservation of the software s initial behavior. The above requirements combined with the constant growth of the size of modern software systems make manual refactoring difficult to be done by software engineers. The value of automation the process of refactoring has been confirmed by the software industry and a plethora of automatic refactoring tools has been developed. However, the majority of automatic refactoring tools support only primitive refactorings, without enabling the automated execution of more complex refactorings, such as refactorings aimed at introducing a design pattern. This thesis intends to contribute a solution to the above problem, by examining the problem of automating the refactoring to Null Object design pattern. The Null Object pattern, based on the principles of inheritance and polymorphism, should be used in cases where repetitive checks if an object is not null before sending a message to it 10
exist throughout the code. The aim of the developed methodology is the automatic identification of cases where it is possible to apply the Null Object design pattern. Moreover, the proposed approach allows, for cases that meet the criteria for automatic implementation of the refactoring, the automatic reconstruction of the source code in order for the structure of the pattern to be implemented. More specifically, the proposed methodology is limited to the detection and removal of conditional logic that checks whether a field is equal or not to null before invocating some of its methods, in order to avoid causing a Null Pointer Exception, since developer is not sure whether the field has been assigned value other than null. In order to define the problem of identification of refactoring to Null Object opportunities, the proposed identification methodology, first, tries to identify in each class of the examined code those of its fields which are of type of some class that belongs to the system under consideration, in order to be possible to apply the refactoring process. These fields are then examined in order to determine whether they meet a set of selected criteria which denote that there is a high probability to exist in the class they are defined conditional expressions that check whether their value equals null or not. Each field that meets the criteria of the previous analysis is characterized as candidate for the application of refactoring to Null Object pattern. It is then examined whether there exist conditional expressions that check whether the field is equal or not to null and which meet a set of criteria which allow their removal, while ensuring that no errors are introduced and maintaining the software behavior. Only the fields for which conditional statements candidates for elimination will be detected, will be finally considered suitable for the application of refactoring to Null Object pattern, since only then will we benefit from the use of the pattern. Moreover, a third set of criteria is examined in order to determine whether for the candidate fields the application of the refactoring to the Null Object pattern can be automated. Otherwise, the proposed methodology will classify this field as suggested refactorable, since it requires the intervention of the programmer in order for the refactoring to Null Object pattern to be applied. 11
Regarding the automatic identification of refactoring to Null Object candidates, the proposed methodology is based on the technique of static analysis of the examined source code in order for its Abstract Syntax Tree (AST) to be produced. The AST tree is analyzed so as to detect the fields and conditional statements that satisfy the preconditions defined by the methodology, while for a specific subset of the identification criteria that the required information could not be drawn from it, the Control Flow Graph (CFG) and the Program Dependence Graph (PDG) were used. The refactoring suggestions that arise as a result of the identification process and are presented to the user, correspond to conditional statements that their branch condition checks the value of a candidate for refactoring field. These suggestions are presented to the user sorted according to whether they can be eliminated, and grouped per candidate field. Then, the user is able to preview the refactoring suggestions one by one and select the ones who wishes to apply. During the application of a user-selected refactoring suggestion, the abstract syntax tree of the examined code is being restructured and the user is given the option to preview the proposed refactoring prior to selecting its application, as well as the option to reject it. When the user confirms the application of the proposed refactoring, the transformations of the AST tree are used in order to modify the source code of the software without user s contribution. During the application of a refactoring to Null Object pattern, the transformations applied to the AST tree intend to apply the structural elements of the pattern in the reconstructed system, as well as to guarantee the preservation of the initial behavior of the system under consideration. The structural elements that make up the Null Object pattern, when following the approach of abstract superclass, is the client class that defines the candidate field, the field for which conditional statements are eliminated, the abstract class and its concrete subclasses. Consequently, the transformations performed during refactoring are summarized in the creation of the abstract class and its concrete Null Object subclass whose methods will have the suitable behavior in order to be able to eliminate as many conditional statements from the program. In addition, the selected field should be declared having as type the abstract superclass and instantiated with an object of the Null Object subclass. Finally, the client class should be appropriately modified, in order for the conditional statements to be 12
eliminated and also for the software behavior to be maintained the same. The proposed methodology has been implemented with the use of Java programming language and also of the JDT and LTK Eclipse libraries and has been integrated in the already existing refactoring tool for Eclipse IDE, JDeodorant, as part of it. The tool JDeodorant, on which the implementation of the proposed methodology was based, is an Eclipse plug-in that allows the automatic application of refactorings which solve the design problems «God Class», «Long Method», «Type Checking» and «Feature Envy». After having constructed the corresponding support tool, the proposed methodology was tested on several open source software systems in order to evaluate its effectiveness both in terms of its usefulness and applicability as well as of its scalability. Particular attention was given to use a large number of software systems in the experimental evaluation that were big in terms of size and complexity. This decision was taken for the sake of completeness, so that the usefulness and scalability of the methodology be examined in several software systems that differ in size and complexity and represent software systems used in practice. In the context of evaluating the usefulness and applicability of the proposed methodology, both cases of fields detected as suitable for automated application of refactoring, and cases of the suggested refactorable fields are recorded for each of the examined software systems. Regarding the evaluation of the scalability of the proposed methodology, the execution time required for the identification process was recorded for each of the examined software systems. The results of the above experiments are being presented and used to infer some useful conclusions. Finally, the present thesis records the limitations and the deficiencies of the proposed methodology of refactoring to Null Object pattern. These limitations can be the subject of future research study to extend and enrich the proposed methodology. 13
Πίνακας περιεχομένων Ευχαριστίες... 4 Περίληψη... 5 Abstract... 10 Πίνακας Πινάκων... 16 Πίνακας Εικόνων... 17 Κεφάλαιο 1 Εισαγωγή... 17 1.1 Σκοπός και Συμβολή της Διπλωματικής Εργασίας... 21 1.2 Δομή της Διπλωματικής Εργασίας... 22 Κεφάλαιο 2 - Αναδόμηση και Πρότυπα Σχεδίασης... 24 2.1 Αναδόμηση Κώδικα... 24 2.1.1 Πλεονεκτήματα και Μειονεκτήματα της Αναδόμησης... 26 2.1.2 Περιπτώσεις Εφαρμογής μιας Αναδόμησης... 28 2.1.3 Κακές Οσμές (Bad Smells)... 29 2.1.4 Είδη Αναδόμησης... 33 2.1.5 Τρόποι και Εργαλεία Αναδόμησης... 37 2.2 Πρότυπα Σχεδίασης... 39 2.3 Αναδόμηση με Πρότυπα Σχεδίασης... 43 Κεφάλαιο 3 - Αυτοματοποίηση Αναδόμησης προς το Πρότυπο Null Object... 47 3.1 Το Πρότυπο Σχεδίασης Null Object... 47 3.2 Χειρωνακτική Αναδόμηση προς το Πρότυπο Null Object... 51 3.3 Προσεγγίσεις Αυτόματης Αναδόμησης με Χρήση του Προτύπου Σχεδίασης Null Object... 56 3.4 Μέθοδοι Ανάλυσης Κώδικα στην Αυτοματοποιημένη Αναδόμηση... 57 3.4.1 Αφηρημένα Συντακτικά Δέντρα (AST)... 58 3.4.2 Γράφος Ροής Ελέγχου (CFG)... 61 3.4.3 Γράφος Εξαρτήσεων Προγράμματος (PDG)... 63 3.4.4 Τεχνική Slicing... 65 Κεφάλαιο 4 Προτεινόμενη Μεθοδολογία... 69 4.1 Τα Προβλήματα της Χρήσης Υποθετικής Λογικής και Διπλότυπου Κώδικα.. 70 4.2 Στόχος της Προτεινόμενης Μεθοδολογίας... 72 4.3 Ανίχνευση Περιπτώσεων Αναδόμησης... 77 14
4.3.1 Εντοπισμός των Υποψήφιων Πεδίων... 78 4.3.2 Εντοπισμός της Δυνατότητας Αυτόματης Εφαρμογής της Αναδόμησης.. 81 4.3.3 Εντοπισμός Υποθετικών Δηλώσεων Υποψήφιων για Διαγραφή... 83 4.3.4 Ανάλυση των Προϋποθέσεων για τις Υποψήφιες για Διαγραφή Υποθετικές Δηλώσεις... 88 4.3.4.1 Ανάλυση των Διαφορετικών Ειδών Υποθετικών Δομών... 89 4.3.5 Εντοπισμός των Suggested Refactorable Πεδίων... 94 4.4 Εφαρμογή Αναδόμησης... 95 4.5 Λεπτομέρειες Υλοποίησης... 105 4.6 Παρουσίαση του Εργαλείου Αναδόμησης... 106 4.7 Παρουσίαση Εκτελούμενης Αναδόμησης... 110 4.8 Συμπεράσματα και Περιορισμοί... 116 Κεφάλαιο 5 - Πειραματική Αξιολόγηση... 119 5.1 Συστήματα Λογισμικού για την Αξιολόγηση... 120 5.2 Αποτελέσματα από την Πειραματική Εκτέλεση της Μεθοδολογίας... 124 5.3 Συμπεράσματα Βάσει των Αποτελεσμάτων... 130 Κεφάλαιο 6 - Συμπεράσματα και Μελλοντική Έρευνα... 138 Παράρτημα... 142 Παράρτημα Α Υλοποίηση Προτεινόμενης Μεθοδολογίας... 142 Παράρτημα Β Οδηγίες Εγκατάστασης του plug-in JDeodorant... 151 Συντομογραφίες... 152 Βιβλιογραφία... 153 15
Πίνακας Πινάκων Πίνακας 1: Οι δώδεκα κακές οσμές του Kerievsky και οι αντίστοιχες αναδομήσεις που αποσκοπούν στην απομάκρυνσή τους... 33 Πίνακας 2: Ο κατάλογος των 72 αναδομήσεων του Fowler και η αντίστοιχη κατηγοριοποίησή τους... 36 Πίνακας 3: Δημοφιλή εργαλεία αυτοματοποιημένης αναδόμησης για τις γλώσσες προγραμματισμού Java, C++ και.net... 39 Πίνακας 4: Τα 23 πρότυπα σχεδίασης της συγγραφικής ομάδας GoF και η κατηγοριοποίησή τους... 43 Πίνακας 5: Οι 27 αναδομήσεις με πρότυπα σχεδίασης του Kerievsky και η κατηγοριοποίησή τους... 46 Πίνακας 6: Χαρακτηριστικά μεγέθους των υπό εξέταση συστημάτων λογισμικού.. 123 Πίνακας 7: Αποτελέσματα αξιολόγησης της μεθοδολογίας στα υπό εξέταση συστήματα λογισμικού... 127 Πίνακας 8: Ποσοστό των κατάλληλων πεδίων για κάθε υπό εξέταση σύστημα λογισμικού... 128 Πίνακας 9: Χρόνοι υπολογισμού για τη διαδικασία ανίχνευσης προτάσεων αναδόμησης προς το πρότυπο Null Object... 130 Πίνακας 10: Οι κλάσεις που χρησιμοποιούνται για την υλοποίηση του γραφικού περιβάλλοντος του εργαλείου... 145 Πίνακας 11: Οι κλάσεις που χρησιμοποιούνται στη διαδικασία ανίχνευσης προτάσεων αναδόμησης... 149 Πίνακας 12: Οι κύριες κλάσεις που χρησιμοποιούνται στη διαδικασία εκτέλεσης μιας αναδόμησης... 151 16
Πίνακας Εικόνων Εικόνα 1: Ο κατάλογος των αναδομήσεων που υποστηρίζει το Eclipse... 39 Εικόνα 2: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας υποκλάσης... 49 Εικόνα 3: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας αφηρημένης κλάσης... 49 Εικόνα 4: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας διεπαφής... 50 Εικόνα 5: Το διάγραμμα κλάσεων του παραδείγματος πριν και μετά την εφαρμογή του προτύπου Null Object... 50 Εικόνα 6: Ο κώδικας του παραδείγματος πριν και μετά την εφαρμογή του προτύπου Null Object [31]... 51 Εικόνα 7: Παράδειγμα κλάσης Hello.java... 59 Εικόνα 8: Απόσπασμα του αφηρημένου συντακτικού δέντρου της κλάσης Hello.java της Εικόνας 7, όπως παράγεται από το ASTViewer plug-in του Eclipse... 60 Εικόνα 9: Τμήμα κώδικα χωρισμένο σε βασικές ενότητες [41]... 62 Εικόνα 10: Γράφος ροής ελέγχου (αριστερά) και γράφος ροής ελέγχου με βασικές ενότητες (δεξιά) για το τμήμα κώδικα της Εικόνας 9 [41]... 62 Εικόνα 11: Ο γράφος εξαρτήσεων κλάσεων για το τμήμα κώδικα της Εικόνας 9, στον οποίο περιέχεται τόσο ο υπογράφος με τις εξαρτήσεις ελέγχου όσο και ο υπογράφος με τις εξαρτήσεις δεδομένων [41]... 64 Εικόνα 12: Ο block-based γράφος εξαρτήσεων ελέγχου (block-based control dependence graph) της μεθόδου statement που παρουσιάστηκε στην Εικόνα 9 [41]. 68 Εικόνα 13: Οι βασικές ενότητες της μεθόδου της Εικόνας 9, οι γραμμοσκιασμένοι leader κόμβοι τους, καθώς και τα boundary blocks για την εντολή της γραμμής 10 [41]... 68 Εικόνα 14: Κώδικας του παραδείγματος Room Customer που θα χρησιμοποιηθεί στην ανάλυση της προτεινόμενης μεθοδολογίας... 76 Εικόνα 15: Διάγραμμα κλάσεων του παραδείγματος Room Customer... 76 Εικόνα 16: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω ύπαρξης εντολής ανάθεσης... 80 17
Εικόνα 17: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω κλήσης setter μεθόδου πρώτης μορφής... 80 Εικόνα 18: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω κλήσης setter μεθόδου δεύτερης μορφής... 80 Εικόνα 19: Περίπτωση κατασκευαστή που θεωρείται ότι θέτει το πεδίο λόγω κλήσης μεθόδου της context κλάσης Room που δεν είναι setter μέθοδος κάποιου άλλου πεδίου της κλάσης Room... 80 Εικόνα 20: Διάγραμμα κλάσεων της μορφής του κώδικα πριν και μετά την εφαρμογή της αναδόμησης... 81 Εικόνα 21: Παραδείγματα υποθετικών δηλώσεων για το πεδίο customer και ο τρόπος που έχουν χαρακτηριστεί από τη μεθοδολογία... 87 Εικόνα 22: Η αλλαγή στη δομή του κώδικα μετά την εφαρμογή της αναδόμησης για το πεδίο customer... 96 Εικόνα 23: Η δημιουργηθείσα κλάση AbstractCustomer... 98 Εικόνα 24: Η δημιουργηθείσα κλάση NullCustomer... 100 Εικόνα 25: Η μορφή της κλάσης Customer πριν και μετά την εφαρμογή της αναδόμησης... 101 Εικόνα 26: Η κλάση Room πριν και μετά την εφαρμογή της αναδόμησης... 104 Εικόνα 27: Το μενού Bad Smells και η όψη Null Checks με τις επιλογές ανίχνευσης, αποθήκευσης και εφαρμογής των προτάσεων αναδόμησης προς το πρότυπο Null Object... 107 Εικόνα 28: Παρουσίαση των προτάσεων αναδόμησης προς το πρότυπο Null Object και προεπισκόπηση της επιλεγμένης υποθετικής δήλωσης που ανιχνεύτηκε... 109 Εικόνα 29: Ο οδηγός για την προεπισκόπηση και τροποποίηση από το χρήστη των προεπιλεγμένων ονομάτων των κλάσεων και των μεθόδων που θα δημιουργηθούν 109 Εικόνα 30: Ο οδηγός για την προεπισκόπηση και αποδοχή ή απόρριψη του προτεινόμενου μετασχηματισμού... 110 Εικόνα 31: Το κατάλληλο για εφαρμογή του προτύπου Null Object πεδίο perm της κλάσης JUnitTestRunner με δυο υποθετικές δηλώσεις υποψήφιες για διαγραφή... 111 Εικόνα 32: Η δήλωση του πεδίου perm πριν την εφαρμογή της αναδόμησης... 111 Εικόνα 33: Κατασκευαστής της κλάσης JUnitTestRunner ο οποίος δε θέτει το πεδίο perm... 112 18
Εικόνα 34: Η δημιουργηθείσα κλάση AbstractPerm... 113 Εικόνα 35: Η δημιουργηθείσα κλάση NullPerm... 113 Εικόνα 36: Η κλάση Permissions που αντιστοιχεί στον τύπο του πεδίου δηλώνεται ως υποκλάση της κλάσης AbstractPerm... 114 Εικόνα 37: Η προσθήκη των μεθόδων isnull() και getreference() στην κλάση Permissions... 114 Εικόνα 38: Η τροποποίηση της δήλωσης του πεδίου perm στην context κλάση JUnitTestRunner... 115 Εικόνα 39: Η προσθήκη της μεθόδου assigntoperm στην context κλάση JUnitTestRunner... 115 Εικόνα 40: Περίπτωση εντολής ανάθεσης που τροποποιήθηκε, καθώς και υποθετικής λογικής που διαγράφηκε... 116 Εικόνα 41: Ραβδόγραμμα όπου συνοψίζονται τα αποτελέσματα της εφαρμογής της μεθοδολογίας σε καθένα εξεταζόμενο σύστημα λογισμικού... 132 Εικόνα 42: Συσχέτιση του χρόνου δημιουργίας του AST με τον αριθμό γραμμών κώδικα του συστήματος (TLOC)... 133 Εικόνα 43: Ραβδόγραμμα όπου συγκρίνονται οι χρόνοι υπολογισμού της διαδικασίας ανίχνευσης προτάσεων αναδόμησης προς το πρότυπο Null Object... 134 Εικόνα 44: Γραφική παράσταση της συσχέτισης μεταξύ του χρόνου ελέγχου των προϋποθέσεων για την ανίχνευση προτάσεων αναδόμησης και του συνολικού αριθμού γραμμών κώδικα (TLOC) στο σύνολο των εξεταζόμενων συστημάτων λογισμικού 136 Εικόνα 45: Γραφική παράσταση της συσχέτισης μεταξύ του χρόνου ελέγχου των προϋποθέσεων για την ανίχνευση προτάσεων αναδόμησης και του αριθμού των εν δυνάμει υποψήφιων πεδίων στο σύνολο των εξεταζόμενων συστημάτων λογισμικού... 137 Εικόνα 46: Διάγραμμα πακέτων στα οποία περιλαμβάνονται οι κλάσεις οι οποίες χρησιμοποιήθηκαν για την υλοποίηση της προτεινόμενης μεθοδολογίας... 142 Εικόνα 47: Διάγραμμα των κλάσεων που χρησιμοποιούνται για την υλοποίηση του γραφικού περιβάλλοντος του εργαλείου αναδόμησης προς το πρότυπο Null Object 145 Εικόνα 48: Οι βασικότερες κλάσεις που χρησιμοποιούνται στη διαδικασία ανίχνευσης προτάσεων αναδόμησης προς το πρότυπο Null Object... 150 Εικόνα 49: Οι βασικότερες κλάσεις που συμμετέχουν στην εκτέλεση μιας 19
αναδόμησης προς το πρότυπο Null Object... 151 20
Κεφάλαιο 1 Εισαγωγή 1.1 Σκοπός και Συμβολή της Διπλωματικής Εργασίας Οι αλλαγές αποτελούν αναπόσπαστο μέρος της ανάπτυξης ενός συστήματος λογισμικού. Αυτό οφείλεται στη δυναμική φύση των συστημάτων λογισμικού, τα οποία συνεχώς ενημερώνονται, είτε προκειμένου να ακολουθήσουν τις τεχνολογικές εξελίξεις, είτε εξαιτίας επιβεβλημένων τροποποιήσεων των απαιτήσεων σχεδίασης, είτε προκειμένου να επεκταθεί η λειτουργικότητά τους και να παραμείνουν βιώσιμα. Ωστόσο, οι διαρκείς τροποποιήσεις ενός συστήματος λογισμικού μπορεί να έχουν αρνητικό αντίκτυπο στην ποιότητα της σχεδίασής του. Η χαμηλή ποιότητα σχεδίασης με τη σειρά της επηρεάζει αρνητικά την κατανοησιμότητα (understandability) του λογισμικού και δυσχεραίνει την επέκταση και τη συντήρησή του (maintenance). Προς την κατεύθυνση της βελτίωσης της ποιότητας σχεδίασης ενός συστήματος λογισμικού στοχεύει η εφαρμογή αναδομήσεων του κώδικα (refactorings). Η διαδικασία της αναδόμησης είναι μια διαδικασία τροποποίησης της δομής του κώδικα με τέτοιο τρόπο ώστε να βελτιωθούν κάποια ποιοτικά χαρακτηριστικά του, χωρίς, όμως, να τροποποιηθεί η εξωτερικά παρατηρούμενη συμπεριφορά του. Μια ακόμα πολύ διαδεδομένη μέθοδος για τη βελτίωση της ποιότητας σχεδίασης ενός συστήματος λογισμικού είναι η χρήση των προτύπων σχεδίασης (design patterns). Τα πρότυπα σχεδίασης είναι μια συστηματική προσπάθεια καταγραφής λύσεων σε κοινά προβλήματα σχεδίασης. Τα πρότυπα σχεδίασης αναπτύχθηκαν βασιζόμενα σε βασικές αρχές της αντικειμενοστραφούς σχεδίασης, όπως ο διαχωρισμός των διεπαφών από την υλοποίηση, η προτίμηση της μεταβίβασης έναντι της κληρονομικότητας και η ενθυλάκωση της μεταβλητότητας. Ως εκ τούτου, η δομή του παραγόμενου τρόπου σχεδίασης του συστήματος λογισμικού επιτρέπει την ευκολότερη τροποποίηση και εν γένει συντήρηση του συστήματος λογισμικού. Στην παρούσα διπλωματική εργασία μελετάται ο συνδυασμός της αναδόμησης με τα πρότυπα σχεδίασης, ώστε να συνδυαστούν τα οφέλη των δύο τεχνικών, με απώτερο σκοπό τη μείωση της πολυπλοκότητας ενός συστήματος λογισμικού, με ταυτόχρονη αύξηση της κατανοησιμότητας, της επαναχρησιμοποιησιμότητας (reusability) και της συντηρησιμότητάς του (maintainability). Πιο συγκεκριμένα, η παρούσα εργασία 21
ασχολείται με το πρόβλημα της αυτοματοποιημένης αναδόμησης προς το σχεδιαστικό πρότυπο Null Αντικείμενο (Null Object). Στον ερευνητικό τομέα της αυτοματοποιημένης αναδόμησης προς κάποιο σχεδιαστικό πρότυπο παρατηρείται σχετική έλλειψη. Ενώ η βιομηχανία λογισμικού έχει επιβεβαιώσει τη σημασία και την αξία της αυτοματοποιημένης αναδόμησης και έχουν αναπτυχθεί αρκετά εργαλεία αυτόματης αναδόμησης, αυτά περιορίζονται κυρίως στην υποστήριξη αναδομήσεων χαμηλού επιπέδου, χωρίς να παρέχουν τη δυνατότητα αυτοματοποιημένης εκτέλεσης πιο σύνθετων αναδομήσεων, όπως οι αναδομήσεις που αποσκοπούν στην εισαγωγή κάποιου σχεδιαστικού προτύπου. Αυτή η έλλειψη οφείλεται κυρίως στην εννοιολογική φύση των προτύπων σχεδίασης, η οποία και καθιστά δύσκολο τον αυτοματοποιημένο εντοπισμό των περιπτώσεων εφαρμογής τους. Αντίστοιχη έλλειψη υπάρχει και στον τομέα της αυτοματοποιημένης αναδόμησης προς το πρότυπο Null Object, την οποία και επιχειρεί να καλύψει η παρούσα διπλωματική εργασία. Η προσέγγιση που αναπτύχθηκε αποσκοπεί τόσο στην αυτοματοποιημένη ανίχνευση των προβληματικών σημείων του κώδικα που ενδείκνυται η εφαρμογή του προτύπου Null Object, όσο και στην αυτόματη εκτέλεση της αναδόμησης. Ιδιαίτερη μέριμνα δόθηκε ώστε η προτεινόμενη μεθοδολογία να είναι αρκετά γενική, ώστε να εντοπίζονται όσο το δυνατόν περισσότερες υποψήφιες περιπτώσεις για την εφαρμογή του προτύπου, αλλά και να εξασφαλίζεται η μη εισαγωγή σφαλμάτων και η διατήρηση της συμπεριφοράς του συστήματος κατά την εφαρμογή της αναδόμησης. 1.2 Δομή της Διπλωματικής Εργασίας Ακολούθως παρουσιάζεται συνοπτικά το περιεχόμενο που πραγματεύεται κάθε κεφάλαιο της παρούσας διπλωματικής εργασίας. Στο Κεφάλαιο 2 αναλύονται οι έννοιες της τεχνολογίας λογισμικού αναφορικά με την αναδόμηση (refactoring), τα πρότυπα σχεδίασης (design patterns) και την αναδόμηση με πρότυπα σχεδίασης (refactoring to patterns). Η ανάλυση αυτή κρίνεται αναγκαία προκειμένου να είναι δυνατή η πλήρης κατανόηση της σημασίας των όρων που χρησιμοποιούνται σε όλη την έκταση της παρούσας διπλωματικής εργασίας. Εν συνεχεία, στο Κεφάλαιο 3, καταρχάς, αναλύεται η δομή και η χρήση του σχεδιαστικού προτύπου Null Object και στη συνέχεια πραγματοποιείται μια 22
επισκόπηση της βιβλιογραφίας τόσο αναφορικά με τη χειρωνακτική αναδόμηση όσο και με την αυτοματοποιημένη αναδόμηση με χρήση του προτύπου Null Object. Ακολούθως, στο Κεφάλαιο 4 παρουσιάζεται αναλυτικά η μεθοδολογία που αναπτύχθηκε στα πλαίσια της παρούσας διπλωματικής εργασίας και παρέχονται εκτενείς πληροφορίες αναφορικά με την υλοποίηση της. Επιπλέον, για λόγους πληρότητας της παρουσίασης της προτεινόμενης προσέγγισης, παρουσιάζεται, μέσω ενός αντιπροσωπευτικού παραδείγματος από πραγματικό σύστημα λογισμικού, η εφαρμογή της αναδόμησης προς το πρότυπο Null Object, όπως δύναται να πραγματοποιηθεί αυτοματοποιημένα μέσω του εργαλείου που αναπτύχθηκε. Τέλος, αναλύονται οι περιορισμοί στους οποίους υπόκειται η προτεινόμενη προσέγγιση αναδόμησης. Προκειμένου να αξιολογηθεί η αποτελεσματικότητα της προτεινόμενης μεθοδολογίας τόσο αναφορικά με τη χρησιμότητα και την εφαρμοσιμότητά της στην πράξη όσο και ως προς την κλιμακωσιμότητά της, κρίθηκε αναγκαία η πειραματική εφαρμογή της σε πραγματικά συστήματα λογισμικού, τα αποτελέσματα της οποίας παρουσιάζονται στο Κεφάλαιο 5. Τέλος, στο Κεφάλαιο 6 συνοψίζεται η συνεισφορά της προτεινόμενης αυτοματοποιημένης μεθόδου αναδόμησης προς το πρότυπο Null Object, ενώ επαναλαμβάνονται οι περιορισμοί της προτεινόμενης προσέγγισης οι οποίοι δύναται να αποτελέσουν ενδιαφέροντα ερευνητικά ζητήματα για περαιτέρω διερεύνηση. 23
Κεφάλαιο 2 - Αναδόμηση και Πρότυπα Σχεδίασης 2.1 Αναδόμηση Κώδικα Τα συστήματα λογισμικού ανεξαρτήτως του σχεδιασμού τους και της γλώσσας ανάπτυξής τους είναι σίγουρο ότι κάποια στιγμή θα πρέπει να επεκταθούν ή, αν μη τι άλλο, να τροποποιηθούν, διαφορετικά κινδυνεύουν να οδηγηθούν σε αχρηστία. Οι αλλαγές σε ένα σύστημα λογισμικού είναι συνήθως αρκετά συχνές και πηγάζουν κυρίως από τροποποιήσεις των απαιτήσεων σχεδίασης, από την ανάγκη τροποποίησης της υπάρχουσας λειτουργικότητας, από την απαίτηση προσθήκης νέων δυνατοτήτων ή από την ανάγκη προσαρμογής στις τεχνολογικές εξελίξεις. Οι αλλαγές σε ένα σύστημα λογισμικού πολλές φορές γίνονται βεβιασμένα και υπό την πίεση προθεσμιών, χωρίς να προηγηθεί η κατάλληλη προσαρμογή του τρόπου σχεδίασης (design) του λογισμικού, ώστε αυτές να ενσωματωθούν με τον πιο πρόσφορο τρόπο. Ως εκ τούτου, η ποιότητα του τρόπου σχεδίασης του λογισμικού πλήττεται σε πολύ μεγάλο βαθμό, κάτι που έχει σοβαρό αντίκτυπο στην κατανοησιμότητα (understandability), στην συντηρησιμότητα (maintainability) και στην επεκτασιμότητα (extendability) του λογισμικού. Δεν είναι τυχαίο άλλωστε που η διαδικασία της συντήρησης και της τροποποίησης ενός συστήματος λογισμικού απαιτεί σχεδόν πάντα πολύ περισσότερο χρόνο από τη διαδικασία ανάπτυξής του. Προς την κατεύθυνση της βελτίωσης της ποιότητας του τρόπου σχεδίασης ενός συστήματος λογισμικού στοχεύει η διαδικασία της αναδόμησης (refactoring). Ο όρος αναδόμηση εμφανίστηκε για πρώτη φορά στη διδακτορική διατριβή του Griswolt [1] το 1991, η οποία και ασχολήθηκε με την αναδόμηση σε προγράμματα που ακολουθούν τις αρχές της συναρτησιακής και διαδικαστικής σχεδίασης (functional and procedural design). Στη συνέχεια, ακολούθησε ο Opdyke [2] το 1992, ο οποίος στη διδακτορική του διατριβή ανέλυσε την αναδόμηση σε αντικειμενοστραφή συστήματα λογισμικού (object-oriented programs) και την όρισε ως τη διαδικασία αναδιάρθρωσης της εσωτερικής δομής του κώδικα, η οποία όμως διατηρεί ίδια τη συμπεριφορά του. Ο Kerievsky αναφέρει ότι ο όρος αναδόμηση αφορά σε ένα μετασχηματισμό του λογισμικού που διατηρεί όμως τη συμπεριφορά του (behavior-preserving transformation) [3], ενώ ο Folwer ορίζει ότι η αναδόμηση 24
είναι η διαδικασία τροποποίησης ενός συστήματος λογισμικού ώστε να μην αλλάζει η εξωτερικά παρατηρήσιμη συμπεριφορά του κώδικα, όμως παρ όλα αυτά να βελτιώνεται η εσωτερική δομή του [4]. Πιο συγκεκριμένα, ο Folwer αναφέρει ότι η έννοια αναδόμηση έχει δύο ορισμούς, έναν που αφορά στη χρήση του όρου ως ουσιαστικό και έναν σαν ρήμα. Αναδόμηση (ουσιαστικό): μια αλλαγή που πραγματοποιείται στην εσωτερική δομή ενός λογισμικού ούτως ώστε να βελτιωθεί η κατανοησιμότητα του και να γίνει οικονομικότερη η τροποποίησή του, χωρίς όμως να αλλάζει η παρατηρήσιμη συμπεριφορά του. Αναδομώ (ρήμα): αναδιαρθρώνω το λογισμικό μέσω της εφαρμογής μιας σειράς αναδομήσεων χωρίς να τροποποιείται η παρατηρήσιμη συμπεριφορά του. Επιπλέον, και ο Beck [5] δίνει έναν ορισμό του όρου, αναφέροντας ότι η αναδόμηση είναι η διαδικασία προσθήκης αξίας σε ένα σύστημα λογισμικού, χωρίς να τροποποιείται η συμπεριφορά του, αλλά μέσω της ενσωμάτωσης κάποιων ποιοτικών χαρακτηριστικών, όπως η κατανοησιμότητα, η απλότητα και η ευελιξία, τα οποία και επιτρέπουν την ταχύτερη ανάπτυξη του λογισμικού. Η διαδικασία της αναδόμησης θα λέγαμε ότι έχει το ρόλο του συμπληρώματος της διαδικασίας του προκαταρκτικού σχεδιασμού (upfront design). Μια άποψη είναι ότι η αναδόμηση μπορεί να είναι η εναλλακτική του προκαταρκτικού σχεδιασμού. Πιο συγκεκριμένα, σε μια τέτοια θεώρηση σχεδιάζεις το λογισμικό χωρίς να κάνεις εκτιμήσεις για τις μελλοντικές επεκτάσεις ή τροποποιήσεις, αλλά μόνο με βάση τις απαιτήσεις που είναι γνωστές εκείνη τη χρονική στιγμή. Στη συνέχεια, το λογισμικό αναδομείται ώστε να βελτιωθεί ο τρόπος σχεδίασής του. Παρόλο που αυτή η προσέγγιση δεν είναι και η πιο αποτελεσματική, πολύ συχνά ωστόσο οδηγεί στη δημιουργία αξιόλογου λογισμικού από πλευράς σχεδίασης. Η πιο συνήθης χρήση της αναδόμησης, βέβαια, είναι ο συνδυασμός της με κάποιας μορφής αρχικό σχεδιασμό. Σε μια τέτοια περίπτωση, δεν καταβάλλεις μεγάλο χρόνο και προσπάθεια στον αρχικό σχεδιασμό ώστε να βρεις την καλύτερη σχεδιαστική λύση. Αντιθέτως, σου αρκεί η εύρεση μιας λογικής λύσης, μιας και ξέρεις ότι στην πορεία, όταν κατανοήσεις καλύτερα το πρόβλημα και οι απαιτήσεις παγιωθούν, μπορείς μέσω της αναδόμησης, εύκολα και με μικρός κόστος, να τη βελτιώσεις. Με 25
τη χρήση της αναδόμηση, λοιπόν, μετατοπίζεται η έμφαση από τη δημιουργία ενός ευέλικτου τρόπου σχεδίασης που θα έχει λάβει υπόψη όλες τις πιθανές μελλοντικές αλλαγές, στη δημιουργία ενός απλού αρχικού τρόπου σχεδίασης του λογισμικού, ο οποίος, όμως, θα είναι εύκολο στην πορεία να αναδομηθεί στην πιο ευέλικτη σχεδιαστική λύση που θα απαιτηθεί από τις μελλοντικές ανάγκες. Με άλλα λόγια η αναδόμηση μπορεί να οδηγήσει στην υιοθέτηση απλούστερων αρχικών σχεδιαστικών λύσεων χωρίς όμως να θυσιαστεί η ευελιξία του τρόπου σχεδίασης του λογισμικού. Αυτή η προσέγγιση κάνει τη σχεδιαστική διαδικασία πιο εύκολη και λιγότερο αγχωτική. Οι δυνατότητες που μας προσφέρει η αναδόμηση μας εισάγουν και την έννοια της εξελικτικής σχεδίασης (evolutionary design). Η βασική ιδέα της εξελικτικής σχεδίασης είναι ότι, δεδομένου ότι η ανάπτυξη του λογισμικού κινείται σε ένα περιβάλλον μεταβαλλόμενων απαιτήσεων, κάθε προσπάθεια βελτιστοποίησης της προκαταρκτικής σχεδίασης, η οποία προηγείται της κωδικοποίησης, κινδυνεύει να μην έχει το προσδοκώμενο αποτέλεσμα. Σύμφωνα, λοιπόν, με την εξελικτική σχεδίαση, η σχεδίαση του λογισμικού δεν περιορίζεται στο στάδιο της προκαταρκτικής σχεδίασης, αλλά γίνεται διαρκώς και κατά τη φάση της υλοποίησης του λογισμικού, μέσω κατάλληλων αναδομήσεων του κώδικα. Εν κατακλείδι, ο σκοπός της αναδόμησης είναι να καταστήσει το λογισμικό πιο κατανοητό και πιο εύκολα τροποποιήσιμο και επεκτάσιμο. Πρέπει να επισημάνουμε ότι ο σκοπός της αναδόμησης είναι εκ διαμέτρου αντίθετος με αυτόν μιας διαδικασίας βελτιστοποίησης της απόδοσης του λογισμικού (performance optimization). Μπορεί και οι δύο διαδικασίες να μην επεμβαίνουν στην εξωτερική συμπεριφορά του λογισμικού, όμως, εν αντιθέσει με την αναδόμηση, μια διαδικασία βελτιστοποίησης της απόδοσης πολλές φορές έχει ως αποτέλεσμα την παραγωγή ενός πιο πολύπλοκου και δυσνόητου κώδικα. 2.1.1 Πλεονεκτήματα και Μειονεκτήματα της Αναδόμησης Ο σκοπός και το πιο σημαντικό θα λέγαμε πλεονέκτημα της αναδόμησης είναι η βελτίωση της κατανοησιμότητας του λογισμικού. Μπορείς να πραγματοποιήσεις πολλές αλλαγές σε ένα σύστημα λογισμικού, μόνο, όμως, αυτές που κάνουν το λογισμικό πιο κατανοητό θεωρούνται αναδομήσεις. Λίγος χρόνος αφιερωμένος σε 26
αναδόμηση του κώδικα, μπορεί να κάνει τον κώδικα πιο ευανάγνωστο και να καταστήσει πιο εμφανή σε τρίτους τον σκοπό που αυτός επιτελεί. Η αναδόμηση, επίσης, βοηθά στη βελτίωση του τρόπου σχεδίασης του λογισμικού. Όπως προαναφέρθηκε, οι συνεχείς αλλαγές του κώδικα του λογισμικού, οι οποίες γίνονται βεβιασμένα και χωρίς οι προγραμματιστές να κατανοούν πλήρως τον τρόπο σχεδίασης του λογισμικού, έχουν αρνητική επίδραση στη δομή και στην ποιότητα της σχεδίασης του λογισμικού. Μέσω της αναδόμησης, λοιπόν, βελτιώνεται ο τρόπος σχεδίασης του λογισμικού και ως εκ τούτου διευκολύνεται η επέκταση, η τροποποίηση και εν γένει η συντήρηση του λογισμικού. Επιπρόσθετα, δεδομένου ότι μέσω της αναδόμησης ο προγραμματιστής αποκτά βαθειά γνώση της λειτουργικότητας και της δομής του κώδικα του λογισμικού, αυτό τον βοηθά στον ευκολότερο εντοπισμό σφαλμάτων (bugs) στον κώδικα. Επίσης, δεδομένης της βελτίωσης της ποιότητας του τρόπου σχεδίασης του λογισμικού που συνεπάγεται η αναδόμηση, συμπεραίνουμε ότι η αναδόμηση, επιπλέον, βοηθά στην επιτάχυνση της διαδικασίας ανάπτυξης του λογισμικού, καθώς και στην αύξηση της παραγωγικότητας των προγραμματιστών. Όντως, ένας καλός και ποιοτικός τρόπος σχεδίασης του λογισμικού είναι καθοριστικής σημασίας για τη γρήγορη ανάπτυξη του κώδικα, αλλά και την εύκολη και γρήγορη επέκταση, τροποποίηση και προσθήκη νέας λειτουργικότητας στο λογισμικό. Η αναδόμηση παρ όλα τα προαναφερθέντα οφέλη, δεν παύει να είναι μια διαδικασία χρονοβόρα και επίπονη. Η εφαρμογή μιας αναδόμησης απαιτεί να καταβληθεί αρκετή προσπάθεια και χρόνος από πλευράς του προγραμματιστή. Επίσης, τα οφέλη μιας αναδόμησης δεν είναι συνήθως άμεσα αντιληπτά και ως εκ τούτου οι υπεύθυνοι του έργου κρατούν συνήθως μια αρνητική στάση απέναντι στην υιοθέτησή της ως καλή πρακτική. Για τους παραπάνω λόγους και κυρίως επειδή η αναδόμηση είναι μια επίπονη διαδικασία, θα πρέπει αυτή να εκτελείται συνεχώς έτσι ώστε το λογισμικό να διατηρεί ανά πάσα στιγμή ένα καλό επίπεδο ποιότητας. Επιπρόσθετα, ένα ακόμα μειονέκτημα της αναδόμησης είναι ότι ενέχει τον κίνδυνο εισαγωγής σφαλμάτων, καθώς και τον κίνδυνο τροποποίησης της παρατηρούμενης συμπεριφοράς του συστήματος, αν δε γίνει προσεχτικά και μέσα από συγκεκριμένα, προβλέψιμα και μικρά βήματα (small steps). Ως εκ τούτου η αναδόμηση πρέπει να είναι καλά δομημένη και να περιλαμβάνει την εφαρμογή μικρών και ασφαλών βημάτων. Επίσης, 27
πολύ σημαντικό είναι να διενεργούνται οι απαραίτητες δοκιμασίες ελέγχου (tests) μετά την εφαρμογή κάθε αλλαγής, ώστε να επιβεβαιώνεται η διατήρηση της συμπεριφοράς του συστήματος λογισμικού. Η εκτέλεση αυτόματων ελέγχων, πριν, κατά τη διάρκεια και μετά από κάποια αναδόμηση είναι το δίχτυ ασφαλείας που θα επικυρώσει μια σωστή αναδόμηση. 2.1.2 Περιπτώσεις Εφαρμογής μιας Αναδόμησης Η διαδικασία της αναδόμησης πρέπει να αποτελεί αναπόσπαστο τμήμα της διαδικασίας ανάπτυξης του λογισμικού. Όπως αναφέρει και ο Fowler [4], η αναδόμηση δεν είναι δυνατόν να προγραμματιστεί και ο προγραμματιστής να είναι υποχρεωμένος να αφιερώνει ένα συγκεκριμένο μέρος του χρόνου ανάπτυξης ανά τακτά χρονικά διαστήματα σε αυτήν. Αντιθέτως, η αναδόμηση πρέπει να ενσωματωθεί ως αναπόσπαστη πρακτική στη διαδικασία ανάπτυξης του λογισμικού και να εκτελείται διαρκώς. Δεν εφαρμόζεις αναδόμηση επειδή πρέπει, αλλά επειδή θα διευκολύνει τη διαδικασία ανάπτυξης. Η αναδόμηση ενδείκνυται, καταρχάς, σε περιπτώσεις που πρέπει να προστεθεί νέα λειτουργικότητα και νέα χαρακτηριστικά στο λογισμικό. Ένας από τους λόγους που η αναδόμηση συνιστάται σε μια τέτοια περίπτωση είναι διότι βοηθάει στην καλύτερη κατανόηση του κώδικα που τροποποιείται. Επίσης, σε περίπτωση που η δομή του σχεδίου του λογισμικού είναι τέτοια που δεν επιτρέπει την προσθήκη της νέας λειτουργικότητας, μέσω της αναδόμησης μπορεί να τροποποιηθεί ο τρόπος σχεδίασης του λογισμικού ώστε να διευκολυνθεί η επέκταση και η συντήρηση του λογισμικού, τώρα αλλά και στο μέλλον. Επιπρόσθετα, η αναδόμηση ενδείκνυται όταν έχει αναφερθεί ένα σφάλμα που πρέπει να διορθωθεί. Η αναφορά ενός σφάλματος καταδεικνύει ότι ο κώδικας του λογισμικού ήταν αρκετά πολύπλοκος και δυσνόητος και για αυτόν το λόγο το σφάλμα δεν είχε εντοπιστεί πριν το λογισμικό δοθεί σε χρήση. Κατά τη διόρθωση ενός σφάλματος, λοιπόν, η αναδόμηση μπορεί να συμβάλει καθοριστικά στον εντοπισμό του, αλλά και στην πρόληψη μελλοντικών σφαλμάτων. Τέλος, κατά τη διάρκεια των επιθεωρήσεων του κώδικα του λογισμικού (code reviews), η εφαρμογή της αναδόμησης μπορεί να βοηθήσει τους προγραμματιστές επιθεωρητές να κατανοήσουν καλύτερα το υπό εξέταση λογισμικό και να διοχετευτεί η γνώση και στα υπόλοιπα μέλη της ομάδας ανάπτυξης του λογισμικού. 28
Αντιθέτως, υπάρχουν και περιπτώσεις που δεν πρέπει να εφαρμόζεται η αναδόμηση. Μία από αυτές είναι, όπως αναφέρει ο Folwer [4], όταν υπάρχουν έντονα σημάδια ότι ο κώδικας πρέπει να γραφτεί από την αρχή (rewrite from scratch). Υπάρχουν περιπτώσεις που ο κώδικας είναι τόσο κακογραμμένος, πολύπλοκος και δυσνόητος ή περιπτώσεις που ο κώδικας δε δουλεύει σωστά, στις οποίες, ενώ μπορεί να εφαρμοστεί αναδόμηση, μοιάζει ευκολότερο ο κώδικας να γραφτεί εκ του μηδενός. Σε αυτές τις περιπτώσεις η αναδόμηση δε μοιάζει ως η καλύτερη λύση. Επίσης, η αναδόμηση θα πρέπει να αποφεύγεται σε περιπτώσεις που πλησιάζει μια αυστηρή προθεσμία παράδοσης του λογισμικού. Παρόλο που η αναδόμηση βελτιώνει την παραγωγικότητα, σε μια τέτοια περίπτωση το κέρδος αυτό θα καταφανεί πολύ αργά, μετά το πέρας της προθεσμίας. Συνεπώς, παρόλο που η αναδόμηση πρέπει να είναι διαρκής και να εφαρμόζεται σε κάθε ευκαιρία, θα πρέπει να συνυπάρχει αρμονικά με τις προτεραιότητες της διοίκησης και να γίνεται την κατάλληλη χρονική στιγμή. Η αναδόμηση, όπως και άλλες μέθοδοι της τεχνολογίας λογισμικού, έχει τα όριά της. Ορισμένες φορές η προσεχτική προκαταρκτική σχεδίαση μπορεί να είναι η πιο δόκιμη προσέγγιση στην κατασκευή του λογισμικού. Για παράδειγμα, η αναδόμηση έχει προβλήματα κλιμάκωσης σε μεγάλα συστήματα λογισμικού. Επιπλέον, υπάρχει δυσκολία να εφαρμοστεί η αναδόμηση σε συστήματα στα οποία δεν έχει δοθεί η απαραίτητη προσοχή στη σχεδίαση της αρχιτεκτονικής τους [6]. Σε μια τέτοια περίπτωση η παράλειψη του καθορισμού της κατάλληλης αρχιτεκτονικής, η οποία θα παρέχει τα ποιοτικά χαρακτηριστικά του λογισμικού, ενδέχεται να μην μπορεί εύκολα να καλυφθεί με αναδόμηση. Τέλος, ένα ακόμα σημείο που δεν επιτρέπεται η αναδόμηση είναι αν αφορά αλλαγές σε διεπαφές για τις οποίες υπάρχει δέσμευση για τη μη τροποποίησή τους. Σε μια τέτοια περίπτωση μια πιθανή λύση είναι να διατηρούνται τόσο η παλιά όσο και η νέα διεπαφή, μέχρι οι χρήστες της να ενημερωθούν και να αντιδράσουν σε αυτήν την αλλαγή. 2.1.3 Κακές Οσμές (Bad Smells) Η γνώση της έννοιας της αναδόμησης, καθώς και του τρόπου που μπορεί να πραγματοποιηθεί μια αναδόμηση, δεν είναι αρκετή προκειμένου να επωφεληθούμε από τα πλεονεκτήματά της. Ιδιαίτερη σημαντικότητα και αξία έχει ο εντοπισμός του 29
σωστού σημείου του κώδικα που χρήζει αναδόμησης. Πολλά είδη αναδόμησης έχουν προταθεί, όμως το να μπορέσεις εύστοχα να εντοπίσεις το σημείο το κώδικα στο οποίο ταιριάζει η εφαρμογή μιας αναδόμησης είναι μια δύσκολη διαδικασία. Προς την κατεύθυνση της καθοδήγησης στον προσδιορισμό της κατάλληλης αναδόμησης που ενδείκνυται στην εκάστοτε περίπτωση στοχεύουν οι λεγόμενες «κακές οσμές» (bad smells), όπως αναφέρθηκαν για πρώτη φορά από τους Beck και Fowler [4] και αναλύθηκαν στη συνέχεια και από τον Kerievsky [3]. Ο όρος κακές οσμές αντιπροσωπεύει σημάδια και ενδείξεις στον κώδικα που υποδηλώνουν την ύπαρξη κάποιου σχεδιαστικού προβλήματος και την ανάγκη βελτίωσης του κώδικα μέσω της εφαρμογής κάποιας αναδόμησης. Τα πιο συνήθη σχεδιαστικά προβλήματα πηγάζουν από την ύπαρξη διπλότυπου κώδικα (duplicated code), δηλαδή τμήματος κώδικα που επαναλαμβάνεται σχεδόν αυτούσιο σε διάφορα σημεία του λογισμικού, από την ύπαρξη πολύπλοκου κώδικα, καθώς και από την ύπαρξη κώδικα που δεν επικοινωνεί με σαφήνεια το σκοπό και τη λειτουργικότητα που επιτελεί. Στα βιβλία τους οι Folwer [4] και Kerievsky [3] αναλύουν διεξοδικά μια σειρά κακών οσμών, ένα μέρος των οποίων παρουσιάζεται στον Πίνακα 1. Σε καθεμία κακή οσμή αποδίδεται ένα όνομα, το οποίο επιλέχτηκε με τέτοιον τρόπο ώστε να περιγράφει με εύστοχο τρόπο το είδος του σχεδιαστικού προβλήματος που αντιπροσωπεύει η οσμή και να επιτρέπει την ανάπτυξη ενός κοινού λεξιλογίου μεταξύ των προγραμματιστών. Το όνομα της οσμής συμπληρώνει μια περιγραφή του σχεδιαστικού προβλήματος, καθώς και μια αναφορά στο ποιες αναδομήσεις ή συνδυασμός αναδομήσεων μπορεί να απομακρύνει το σχεδιαστικό πρόβλημα της οσμής. Αυτό που πρέπει να τονιστεί, βέβαια, είναι ότι ούτε ο Fowler ούτε ο Kerievsky αναφέρουν συγκεκριμένα ακριβή κριτήρια για το πότε πρέπει να εφαρμοστεί κάποια αναδόμηση στον κώδικα. Εξάλλου, είδαμε ότι κάθε σχεδιαστικό πρόβλημα επιλύεται μέσω της εφαρμογής πολλών διαφορετικών αναδομήσεων. Συνεπώς, το πρόβλημα του προσδιορισμού της κατάλληλης αναδόμησης που ενδείκνυται σε κάθε σημείο του κώδικα, απαιτεί την ανθρώπινη λογική και το ανθρώπινο ένστικτο, αλλά και την εις βάθος κατανόηση της δομής και της σημασιολογίας του κώδικα από πλευράς του προγραμματιστή. 30
Κακή οσμή Διπλότυπος Κώδικας (Duplicated Code) Εκτενής Μέθοδος (Long Method) Υποθετική Πολυπλοκότητα (Conditional Complexity) Εμμονή με Πρωταρχικούς Τύπους (Primitive Obsession) Περιγραφή του σχεδιαστικού προβλήματος Επανάληψη του ίδιου τμήματος κώδικα σε διαφορετικά σημεία του συστήματος. Εκτενής μέθοδος που προκύπτει λόγω της επανάληψης κώδικα, της ύπαρξης εκτενών υποθετικών δηλώσεων για το χειρισμό διαφορετικών αιτήσεων, της ύπαρξης υποθετικών δηλώσεων για την επιλογή διαφορετικών εκδόσεων ενός αλγορίθμου, κτλ.. Ανάλογα με την αιτία του προβλήματος ενδείκνυται διαφορετικός τύπος αναδόμησης. Υπερβολική χρήση πολύπλοκης υποθετικής λογικής με πολλές και εκτενείς διακλαδώσεις αντί για την αξιοποίηση χρήσιμων αρχών της αντικειμενοστραφούς σχεδίασης, όπως η κληρονομικότητα και ο πολυμορφισμός. Υπερβολική χρήση δεδομένων πρωτογενών τύπων (συμβολοσειρών, ακεραίων, δεκαδικών, πινάκων, κτλ.) αντί για τη γενίκευση και την απλοποίηση του κώδικα μέσω της ομαδοποίησης εννοιολογικά συσχετισμένων δεδομένων σε Προτεινόμενη αναδόμηση Form Template Method, Introduce Polymorphic Creation with Factory Method, Chain Constructors, Replace One/Many Distinctions with Composite, Extract Composite, Unify Interfaces with Adapter, Introduce Null Object Compose Method, Move Accumulation to Collecting Parameter, Replace Conditional Dispatcher with Command, Move Accumulation to Visitor, Replace Conditional Logic with Strategy Replace Conditional Logic with Strategy, Move Embellishment to Decorator, Replace State Altering Conditionals with State, Introduce Null Object Replace Type Code with Class, Replace State Altering Conditionals with State, Replace Conditional Logic with Strategy, Replace Implicit Tree with Composite, 31
Απρεπής Έκθεση (Indecent Exposure) Εκτεταμένη Λύση (Solution Sprawl) Εναλλακτικές Κλάσεις με Διαφορετικές Διεπαφές (Alternative Classes with Different Interfaces) κλάσεις. Μέθοδοι και κλάσεις που δε θα έπρεπε να είναι ορατές στους πελάτες είναι δημόσια ορατές στον καθένα. Η έκθεση αυτού του κώδικα υπονοεί ότι οι κλάσεις πελάτες γνωρίζουν την ύπαρξη κώδικα που είναι ασήμαντος και αχρείαστος σε αυτούς. Όταν ο κώδικας ή/και τα δεδομένα που απαιτούνται για να εκτελεστεί μια λειτουργικότητα εκτείνονται σε διάφορες κλάσεις του συστήματος. Αυτό το σχεδιαστικό πρόβλημα συνήθως είναι αποτέλεσμα της γρήγορης προσθήκης ενός χαρακτηριστικού στο σύστημα χωρίς την απαραίτητη προσαρμογή του τρόπου σχεδίασης του συστήματος ώστε να δεχθεί το νέο χαρακτηριστικό με τον καλύτερο δυνατό τρόπο. Αυτή η κακή οσμή παρουσιάζεται όταν οι διεπαφές δύο κλάσεων είναι διαφορετικές, παρ' όλα αυτά όμως, οι κλάσεις είναι σχεδόν όμοιες. Σε μια τέτοια περίπτωση μπορείς να αναδομήσεις τις κλάσεις ώστε να τις κάνεις να μοιράζονται την ίδια διεπαφή. Replace Implicit Language with Interpreter, Move Embellishment to Decorator, Encapsulate Composite with Builder Encapsulate Classes with Factory Move Creation Knowledge to Factory Unify Interfaces with Adapter Τεμπέλικη Κλάση (Lazy Class) Μια κλάση που δεν έχει πολλές αρμοδιότητες για να δικαιολογείται η ύπαρξή της. Inline Singleton Εκτενής Κλάση Κατ αντιστοιχία με το Replace Conditional 32
(Large/God Class) σχεδιαστικό πρόβλημα Long Method, πρόκειται για κλάση που αναλαμβάνει την υλοποίηση πολλών διαφορετικών αρμοδιοτήτων. Dispatcher with Command, Replace State Altering Conditionals with State, Replace Implicit Language with Interpreter Δηλώσεις Switch (Switch Statements) Συνδυαστική Έκρηξη (Combinatorial Explosion) Περίεργη Λύση (Oddball Solution) Χρήση πολλών υποθετικών δηλώσεων αντί του πολυμορφισμού, που επαναλαμβάνονται σε διαφορετικά σημεία του κώδικα και περιπλέκουν τον τρόπο σχεδίασης του λογισμικού. Αυτό το σχεδιαστικό πρόβλημα αποτελεί λεπτή διαφοροποίηση του προβλήματος του διπλότυπου κώδικα. Εμφανίζεται όταν υπάρχουν πολλά σημεία κώδικα που επιτελούν την ίδια λειτουργικότητα χρησιμοποιώντας διαφορετικά είδη ή ποσότητες δεδομένων ή αντικειμένων. Αυτή η κακή οσμή εμφανίζεται όταν κάποιο πρόβλημα επιλύεται με κάποιον τρόπο σε κάποιο σημείο του συστήματος και με κάποιο διαφορετικό τρόπο σε άλλο σημείο του συστήματος, πράγμα που σημαίνει ότι η μία λύση είναι περίεργη ή μη συνεπής. Replace Conditional Dispatcher with Command, Move Accumulation to Visitor Replace Implicit Language with Interpreter Unify Interfaces with Adapter Πίνακας 1: Οι δώδεκα κακές οσμές του Kerievsky και οι αντίστοιχες αναδομήσεις που αποσκοπούν στην απομάκρυνσή τους 2.1.4 Είδη Αναδόμησης Στη βιβλιογραφία [2], [3] οι αναδομήσεις κατατάσσονται σε δύο κύριες κατηγορίες: στις αναδομήσεις χαμηλού επιπέδου (low-level refactorings) και στις σύνθετες αναδομήσεις (composite refactorings). Οι αναδομήσεις χαμηλού επιπέδου αφορούν σε απλούς μετασχηματισμούς του 33
κώδικα του λογισμικού που γίνονται σε επίπεδο μεταβλητής, μεθόδου ή κλάσης. Παραδείγματα τέτοιων αναδομήσεων είναι τα εξής: Extract Method (που μετακινεί κώδικα σε μια νέα μέθοδο), Pull Up Method (που μετακινεί μια μέθοδο από την υποκλάση στην υπερκλάση), Extract Class (που μετακινεί κώδικα σε μια νέα κλάση), αλλά και η αναδόμηση Move Method (που μετακινεί μια μέθοδο από μια κλάση σε κάποια άλλη). Από την άλλη, οι σύνθετες αναδομήσεις αφορούν σε αναδομήσεις υψηλού επιπέδου οι οποίες αποτελούνται από αναδομήσεις χαμηλού επιπέδου. Όλες οι αναδομήσεις που παρουσιάστηκαν στην τρίτη στήλη του Πίνακα 1 είναι σύνθετες αναδομήσεις. Στην περίπτωση των σύνθετων αναδομήσεων, ένας αριθμός αναδομήσεων χαμηλού επιπέδου εφαρμόζεται στο τμήμα του κώδικα που θέλουμε να τροποποιηθεί, μέχρις ότου επιτευχθεί η επιθυμητή αλλαγή. Μεταξύ των εφαρμογών των αναδομήσεων χαμηλού επιπέδου, επιβάλλεται να διενεργούνται δοκιμασίες ελέγχου ώστε να επιβεβαιώνεται η διατήρηση της συμπεριφοράς του κώδικα του λογισμικού και να μπορεί να συνεχιστεί η εφαρμογή των εναπομεινασών αναδομήσεων χαμηλού επιπέδου με βεβαιότητα ως προς την ορθότητα της διαδικασίας. Τα βιβλία των Fowler [4] και Kerievsky [3] αποτελούν την κύρια αναφορά στον τομέα της αναδόμησης. Και οι δύο παρέχουν στον αναγνώστη έναν εκτενή κατάλογο αναδομήσεων. Ο Fowler παρέχει έναν κατάλογο 72 αναδομήσεων, τόσο χαμηλού επιπέδου όσο και σύνθετων, τις οποίες και κατηγοριοποιεί ανάλογα με την λειτουργικότητά τους στις κατηγορίες που φαίνονται στον Πίνακα 2. Ο Kerievsky κατ αντιστοιχία αναλύει ένα σύνολο 21 αναδομήσεων, οι οποίες και παρουσιάστηκαν στην τρίτη στήλη του Πίνακα 1. Η προσπάθεια του Folwer, αλλά και του Kerievsky στη συνέχεια, να καταγράψει και να οργανώσει με συστηματικό τρόπο έναν εκτενή κατάλογο αναδομήσεων αποτελεί αναμφίβολα τη μεγαλύτερη συμβολή στον τομέα της αναδόμησης του κώδικα με σκοπό τη βελτίωσή του. Για την παρουσίαση κάθε αναδόμησης ο Folwer ακολουθεί ένα συγκεκριμένο πρότυπο. Κάθε αναδόμηση που αναλύεται αποτελείται από τα ακόλουθα μέρη: Ένα όνομα (name), το οποίο έχει καταβληθεί προσπάθεια να αντιπροσωπεύει με τον καλύτερο δυνατό και πιο εύστοχο τρόπο την εκτελούμενη αναδόμηση. 34
Η ονοματοθεσία των αναδομήσεων είναι αναγκαία ώστε να αναπτυχθεί ένα κοινό λεξιλόγιο για τον τομέα. Μια σύντομη περιγραφή (summary) της κατάστασης στην οποία ενδείκνυται η εφαρμογή της αναδόμησης, καθώς και μια περιγραφή του ποιες ενέργειες περιλαμβάνει η αναδόμηση. Το κίνητρο (motivation) για την εφαρμογή της αναδόμησης, καθώς και αναφορά σε περιπτώσεις στις οποίες δεν ενδείκνυται η εφαρμογή της. Μια βήμα προς βήμα περιγραφή του αλγορίθμου (mechanics) που πρέπει να ακολουθηθεί για την εφαρμογή της αναδόμησης. Τέλος, παραδείγματα (examples) που δείχνουν με απλό τρόπο πως λειτουργεί η αναδόμηση. Κατηγορία Αναδόμησης Σύνθεσης Μεθόδων (Composing Methods) Μεταφοράς Γνωρισμάτων Μεταξύ Αντικειμένων (Moving Features Between Objects) Οργάνωσης Δεδομένων (Organizing Data) Αναδόμηση Extract Method Inline Method Replace Temp with Query Introduce Explaining Variable Split Temporary Variable Remove Assignments to Parameters Replace Method with Method Object Substitute Algorithm Move Method Move Field Extract Class Inline Class Hide Delegate Remove Middle Man Introduce Foreign Method Introduce Local Extension Self Encapsulate Field Replace Data Value with Object Change Value to Reference Change Reference to Value Replace Array with Object Duplicate Observed Data Change Unidirectional Association to Bidirectional Change Bidirectional Association to Unidirectional Replace Magic Number with Symbolic Constant Encapsulate Field 35
Απλοποίησης Υποθετικών Εκφράσεων (Simplifying Conditional Expressions) Απλοποίησης Κλήσεων Μεθόδων (Making Method Calls Simpler) Αντιμετώπισης Γενίκευσης (Dealing with Generalization) Μεγάλες Αναδομήσεις (Big Refactorings) Encapsulate Collection Moving Behavior into the Class Replace Record with Data Class Replace Type Code with Class Replace Type Code with Subclasses Replace Type Code with Strategy/State Replace Subclass with Fields Decompose Conditional Consolidate Conditional Expression Consolidate Duplicate Conditional Fragments Remove Control Flag Replace Nested Conditional with Guard Clauses Replace Conditional with Polymorphism Introduce Null Object Introduce Assertion Rename Method Add Parameter Remove Parameter Separate Query from Modifier Parameterize Method Replace Parameter with Explicit Methods Preserve Whole Object Replace Parameter with Method Introduce Parameter Object Remove Setting Method Hide Method Replace Constructor with Factory Method Encapsulate Downcast Replace Error Code with Exception Replace Exception with Test Pull Up Field Pull Up Method Pull Up Constructor Body Push Down Method Push Down Field Extract Subclass Extract Superclass Extract Interface Collapse Hierarchy Form Template Method Replace Inheritance with Delegation Replace Delegation with Inheritance Tease Apart Inheritance Convert Procedural Design to Objects Separate Domain From Presentation Extract Hierarchy Πίνακας 2: Ο κατάλογος των 72 αναδομήσεων του Fowler και η αντίστοιχη κατηγοριοποίησή τους 36
2.1.5 Τρόποι και Εργαλεία Αναδόμησης Η αναδόμηση είναι μια διαδικασία δύσκολη και επίπονη. Μια αναδόμηση για να πραγματοποιηθεί σωστά και με ασφάλεια απαιτεί αρκετή προσπάθεια και χρόνο. Για τους παραπάνω λόγους, καθίσταται εμφανές ότι η χειρωνακτική (manual) εκτέλεση μιας αναδόμησης είναι αρκετά απαιτητική διαδικασία. Ιδιαίτερη προσπάθεια πρέπει να καταβληθεί ώστε αφενός να μην εισαχθούν σφάλματα κατά τη διενέργεια μιας χειρωνακτικής αναδόμησης και αφετέρου να διατηρηθεί ίδια η συμπεριφορά του λογισμικού. Η διενέργεια μιας αναδόμησης καθίσταται ακόμα πιο δυσχερής λόγω του μεγάλου μεγέθους των σύγχρονων συστημάτων λογισμικού, ενώ γίνεται ακόμα πιο χρονοβόρα και πολύπλοκη, καθώς είναι απαραίτητη η διενέργεια πρόσθετων δοκιμασιών ελέγχου μετά από κάθε μετασχηματισμό ώστε να επιβεβαιωθεί η απουσία σφαλμάτων. Για όλους τους προαναφερθέντες λόγους κρίνεται αναγκαία η αυτοματοποίηση της διαδικασίας της αναδόμησης. Το πρώτο εργαλείο αυτοματοποιημένης αναδόμησης είναι το Smalltalk Refactoring Browser, το οποίο δημιουργήθηκε to 1997 από τους John Brant και Don Roberts και αφορούσε τη γλώσσα προγραμματισμού Smalltalk [7], [8]. Από τότε πληθώρα εργαλείων αυτόματης αναδόμησης έχουν αναπτυχθεί, τα οποία υποστηρίζουν διάφορες γλώσσες προγραμματισμού και συνήθως ενσωματώνονται στα περιβάλλοντα ανάπτυξης λογισμικού (Integrated Development Environment, IDE). Τα εργαλεία αναδόμησης μπορεί να είναι ημιαυτόνομα (semi-automatic) ή και πλήρως αυτοματοποιημένα (full-automatic). Στην περίπτωση των ημιαυτόνομων εργαλείων απαιτείται η συμμετοχή των προγραμματιστών ώστε να υποδείξουν το σημείο του κώδικα που θέλουν να εφαρμοστεί η αναδόμηση, αλλά και το είδος της αναδόμησης που απαιτείται. Από την άλλη, τα πλήρως αυτοματοποιημένα εργαλεία υποστηρίζουν τη διαδικασία της αναδόμησης τόσο από πλευράς αυτόματης ανίχνευσης των προβληματικών σημείων του κώδικα όσο και σε επίπεδο αυτοματοποιημένης εκτέλεσης των μετασχηματισμών της αναδόμησης. Βέβαια, ακόμα και στην περίπτωση των αυτοματοποιημένων εργαλείων αναδόμησης η συμβολή των προγραμματιστών είναι αδύνατον να εξαλειφθεί εντελώς. Ο προγραμματιστής, αν μη τι άλλο, πρέπει να επιλέξει την αναδόμηση για την οποία θέλει να ανιχνευτούν υποψήφιες προτάσεις, αλλά και να επιθεωρήσει τον 37
προτεινόμενο μετασχηματισμό προτού επιλέξει την εφαρμογή του. Σε πολλές περιπτώσεις, επιπλέον, απαιτείται να προσδιορίσει τις τιμές των παραμέτρων που είναι απαραίτητες προκειμένου να εφαρμοστεί μια αναδόμηση, όπως για παράδειγμα να προσδιορίσει το νέο όνομα της μεθόδου κατά την εφαρμογή της αναδόμησης της μετονομασίας μιας μεθόδου. Παρά τους προαναφερθέντες περιορισμούς των αυτοματοποιημένων εργαλείων αναδόμησης, η χρησιμότητά τους είναι αδιαμφισβήτητη. Τα εργαλεία αυτά είναι απαραίτητα, μιας και κάνουν τη διαδικασία της αναδόμησης γρήγορη, αξιόπιστη και ασφαλή. Τα αυτοματοποιημένα εργαλεία εξασφαλίζουν τον κώδικα από την εισαγωγή σφαλμάτων ή την τροποποίηση της συμπεριφοράς του, ενώ μπορούν να χρησιμοποιηθούν για την ανίχνευση και εφαρμογή προτάσεων αναδόμησης ακόμα και σε συστήματα λογισμικού μεγάλου μεγέθους. Τέλος, δίνουν τη δυνατότητα της αναίρεσης μιας εφαρμοσμένης αναδόμησης σε περίπτωση που τελικά κριθεί μη επιθυμητή. Στον Πίνακα 3 έχουν καταγραφεί κάποια δημοφιλή εργαλεία αυτόματης αναδόμησης για τις αντικειμενοστραφείς γλώσσες προγραμματισμού Java, C++ και.net. Επιπλέον, στην Εικόνα 1 φαίνονται οι αναδομήσεις που υποστηρίζονται από το περιβάλλον ανάπτυξης λογισμικού Eclipse. Τα περισσότερα από αυτά τα εργαλεία υποστηρίζουν αρκετές από τις αναδομήσεις χαμηλού επιπέδου που περιγράφουν οι Fowler [4] και Kerievsky [3]. Όμως, σχετική έλλειψη υπάρχει αναφορικά με την αυτοματοποιημένη ανίχνευση και εκτέλεση σύνθετων αναδομήσεων και πιο συγκεκριμένα, αναδομήσεων που αποσκοπούν στην εισαγωγή κάποιου σχεδιαστικού προτύπου (design pattern). Την έλλειψη αυτή προτίθεται να μειώσει η παρούσα διπλωματική εργασία μέσω της αυτοματοποίησης της αναδόμησης προς το πρότυπο Null Αντικείμενο (Null Object). Γλώσσα Προγραμματισμού Java Εργαλείο Αυτοματοποιημένης Αναδόμησης Eclipse [9], Netbeans [10], JDeodorant [11], IntelliJ IDEA [12], JDeveloper [13], Refactorit [14], JRefactory [15] C++ Visual Assist X [16], XRefactory [17] 38
.NET ReSharper [18], JustCode [19], Refactor! Pro [20] Πίνακας 3: Δημοφιλή εργαλεία αυτοματοποιημένης αναδόμησης για τις γλώσσες προγραμματισμού Java, C++ και.net Εικόνα 1: Ο κατάλογος των αναδομήσεων που υποστηρίζει το Eclipse 2.2 Πρότυπα Σχεδίασης Η έννοια του προτύπου εμφανίστηκε για πρώτη φορά στον τομέα της Αρχιτεκτονικής κτιρίων από τον αρχιτέκτονα Alexander. Συγκεκριμένα, στo βιβλίο του A Pattern Language [21] το 1979, όρισε το αρχιτεκτονικό πρότυπο ως «μια λύση σε ένα πρόβλημα που εμφανίζεται μέσα σε ένα συγκεκριμένο πλαίσιο». Αναφορικά με την επιστήμη της τεχνολογίας λογισμικού, ενώ πολλοί επιστήμονες ασχολήθηκαν με τα σχεδιαστικά πρότυπα (design patterns) στις αρχές της δεκαετίας του 90, το έργο που άσκησε την μεγαλύτερη επιρροή στην επιστημονική κοινότητα 39
ήταν το βιβλίο Design Patterns: Elements of Reusable Object-Oriented Software των συγγραφέων Gamma, Helm, Johnson και Vlissides που δημοσιεύθηκε το 1994 [22]. Σε αναγνώριση της συμβολής τους και της σημαντικότητας του έργου τους, αυτοί οι τέσσερις συγγραφείς είναι γνωστοί με το χαρακτηρισμό Gang of Four (GoF). Η μεγαλύτερη συμβολή του βιβλίου της συγγραφικής ομάδας GoF, είναι η καταγραφή και τεκμηρίωση 23 προτύπων σχεδίασης. Η ιδέα της χρήσης των προτύπων έχει στο μεταξύ επεκταθεί και πέρα του τομέα της σχεδίασης. Έχουμε, λοιπόν, πρότυπα περιπτώσεων χρήσης [23], πρότυπα ανάλυσης [24], αρχιτεκτονικά πρότυπα [25], ενώ τέλος υπάρχουν και τα αντι-πρότυπα (anti-patterns) που είναι διαδεδομένες, αλλά εσφαλμένες πρακτικές στην ανάπτυξη λογισμικού [26]. Τα πρότυπα σχεδίασης είναι μια συστηματική προσπάθεια καταγραφής λύσεων σε κοινά προβλήματα σχεδίασης. Η βασική ιδέα είναι ότι οι μηχανικοί λογισμικού αντιμετωπίζουν τα ίδια προβλήματα σχεδίασης, πολλές φορές ανεξάρτητα από το λογισμικό που σχεδιάζουν. Με τα πρότυπα σχεδίασης εντοπίζονται αυτά τα κοινά και επαναλαμβανόμενα προβλήματα της σχεδίασης λογισμικού και παρέχονται έτοιμες ή σχεδόν έτοιμες λύσεις σχεδίασης. Τα πρότυπα σχεδίασης παρέχουν σχεδιαστικές λύσεις που έχουν δοκιμαστεί και η αποτελεσματικότητά τους έχει αποδειχθεί. Τα πρότυπα σχεδίασης αναπτύχθηκαν βασιζόμενα σε βασικές αρχές της αντικειμενοστραφούς σχεδίασης, όπως ο διαχωρισμός των διεπαφών από την υλοποίηση, η προτίμηση της μεταβίβασης έναντι της κληρονομικότητας και η ενθυλάκωση της μεταβλητότητας. Η μελέτη και η κατανόηση, λοιπόν, των προτύπων σχεδίασης βοηθά το μηχανικό λογισμικού στην εμβάθυνση της αντικειμενοστραφούς σκέψης. Επιπλέον, με την υιοθέτηση των καθιερωμένων σχεδιαστικών προτύπων, ο μηχανικός λογισμικού επωφελείται από την εμπειρία των άλλων και δε χρειάζεται να επινοήσει ο ίδιος λύσεις για κοινά και επαναλαμβανόμενα προβλήματα [27], [28], [29]. Αυτό που κάνει τα πρότυπα σχεδίασης ιδιαίτερα ελκυστικά στην εκμάθησή τους είναι ο τρόπος παρουσίασής τους. Η περιγραφή των σχεδιαστικών προτύπων ακολουθεί ένα συγκεκριμένο πρότυπο παρουσίασης. Το πρότυπο παρουσίασης που υιοθετήθηκε στο βιβλίο των GoF [22] αποτελείται από τις ακόλουθες ενότητες: Όνομα προτύπου: Το όνομα και η κατηγοριοποίηση του προτύπου. Το όνομα 40
ενός προτύπου είναι εξαιρετικά σημαντικό γιατί από μόνο του αρκεί για να επικοινωνήσουν οι μηχανικοί λογισμικού τη σχεδιαστική λύση που προσφέρει το πρότυπο. Σκοπός: Ο σκοπός του προτύπου και το σχεδιαστικό πρόβλημα που επιλύει. Συνώνυμα: Άλλα ονόματα με τα οποία είναι γνωστό το πρότυπο. Έναυσμα: Ένα σενάριο που επιδεικνύει το σχεδιαστικό πρόβλημα που επιλύει το πρότυπο. Εφαρμογή: Περιγράφονται οι συνθήκες κάτω από τις οποίες είναι εφαρμόσιμο το πρότυπο. Δομή: Ένα διάγραμμα κλάσεων ή/και ακολουθίας που δείχνει γραφικά το πρότυπο. Συμμετέχοντες: Οι κλάσεις και τα αντικείμενα που συμμετέχουν στο πρότυπο με τις αρμοδιότητές τους. Συνεργασίες: Ο τρόπος που συνεργάζονται οι συμμετέχοντες στο πρότυπο με τις αρμοδιότητές τους. Συνέπειες: Τα αποτελέσματα εφαρμογής του προτύπου. Υλοποίηση: Ο τρόπος ή οι τρόποι υλοποίησης του προτύπου. Κώδικας: Παραδείγματα κώδικα που δείχνουν την υλοποίηση του προτύπου. Γνωστές χρήσεις: Η χρήση του προτύπου σε πραγματικά συστήματα λογισμικού. Συσχετιζόμενα πρότυπα: Άλλα πρότυπα σχεδίασης που έχουν στενή σχέση με το υπό εξέταση πρότυπο. Καθένα από τα 23 πρότυπα σχεδίασης των συγγραφέων GoF λύνει διαφορετική κατηγορία προβλημάτων και ως εκ τούτου τα σχεδιαστικά πρότυπα χωρίζονται σε τρεις βασικές κατηγορίες: τα κατασκευαστικά πρότυπα (creational patterns), τα δομικά πρότυπα (structural patterns) και τα συμπεριφορικά πρότυπα (behavioral patterns). Τα κατασκευαστικά πρότυπα σχετίζονται με διαδικασίες δημιουργίας αντικειμένων. Τα δομικά πρότυπα ασχολούνται με τη σύνθεση κλάσεων, ενώ τα συμπεριφορικά πρότυπα αφορούν στον τρόπο με τον οποίον αλληλεπιδρούν τα αντικείμενα και στον τρόπο που διανέμονται οι αρμοδιότητες σε κλάσεις. Στον 41
Πίνακα 4 παρουσιάζονται κατηγοριοποιημένα τα 23 πρότυπα σχεδίασης της συγγραφικής ομάδας GoF [22]. Στην παρούσα διπλωματική εργασία μελετάται το πρότυπο Null Αντικείμενο (Null Object) και η αναδόμηση προς αυτό το πρότυπο. Η συγγραφική ομάδα των GoF δεν είχε συμπεριλάβει στoν κατάλογο των σχεδιαστικών της προτύπων το πρότυπο Null Object. Στα χρόνια που ακολούθησαν την έκδοση του βιβλίου τους, πολλοί προγραμματιστές, μέσα από την εμπειρία τους, εντόπισαν και άλλα σχεδιαστικά πρότυπα. Η βιομηχανία του λογισμικού είδε την έκδοση πολλών βιβλίων για σχεδιαστικά πρότυπα, ενώ πολλά συνέδρια σχετικά με το θέμα διενεργήθηκαν. Το πρότυπο Null Object πήρε τη θέση που του αξίζει στη βιβλιογραφία το 1996, μέσω του άρθρου του Bobby Woolf με τίτλο «The Null Object Pattern» [30]. Από τότε πολλοί μηχανικοί λογισμικού έχουν ασχοληθεί με το πρότυπο, όπως οι Henney [31], Fowler [4] και Kerievsky [3]. Δεν είναι τυχαίο άλλωστε ότι ο ίδιος ο Erich Gamma, ένας εκ των συγγραφέων της ομάδας GoF, σε σχετική του συνέντευξη το 2009 [32], είχε αναφέρει ότι στον κατάλογο των σχεδιαστικών τους προτύπων το πρώτο από τα πρότυπα που θα πρόσθετε είναι το πρότυπο Null Object. Κατηγορία Κατασκευαστικά Πρότυπα Σχεδίασης Δομικά Πρότυπα Σχεδίασης Όνομα του προτύπου Abstract Factory (Αφηρημένο Εργοστάσιο) Builder (Κτίστης) Prototype (Πρωτότυπο) Singleton (Μοναδιαίο) Factory Method (Μέθοδος Εργοστάσιο) Adapter (Προσαρμογέας) Bridge (Γέφυρα) Composite (Σύνθετο) Decorator (Διακοσμητής) Façade (Πρόσοψη) Proxy (Αντιπρόσωπος) 42
Συμπεριφορικά Πρότυπα Σχεδίασης Interpreter (Διερμηνέας) Template Method (Μέθοδος Υπόδειγμα) Chain of Responsibility (Αλληλουχία Αρμοδιοτήτων) Command (Εντολή) Iterator (Επαναλήπτης) Mediator (Μεσολαβητής) Memento (Υπόμνηση) Flyweight Observer (Παρατηρητής) State (Κατάσταση) Strategy (Στρατηγική) Visitor (Επισκέπτης) Πίνακας 4: Τα 23 πρότυπα σχεδίασης της συγγραφικής ομάδας GoF και η κατηγοριοποίησή τους 2.3 Αναδόμηση με Πρότυπα Σχεδίασης Τα πρότυπα σχεδίασης, όπως προαναφέρθηκε, προσφέρουν κομψές και ευέλικτες λύσεις σε κοινά προβλήματα σχεδίασης. Η μεγάλη δύναμη των προτύπων σχεδίασης βρίσκεται στη βελτίωση της ποιότητας του τρόπου σχεδίασης του λογισμικού που προσφέρουν, κάτι που συνεπάγεται αύξηση της ευελιξίας, της επεκτασιμότητας, της επαναχρησιμοποιησιμότητας και της συντηρησιμότητας του λογισμικού. Συχνά, όμως, η πρόωρη υιοθέτηση κάποιου προτύπου σχεδίασης κατά τη φάση της σχεδίασης του λογισμικού μπορεί να περιπλέξει τον κώδικα, ενώ θα επαρκούσαν και απλούστερες σχεδιαστικές λύσεις (over-engineering). Επιπλέον, δεν είναι πάντα προφανές ότι για κάποιο σχεδιαστικό πρόβλημα θα πρέπει να χρησιμοποιηθεί κάποιο πρότυπο, με αποτέλεσμα να οδηγούμαστε σε ελλιπή σχεδίαση (under-engineering), με άμεσο επακόλουθο την παραγωγή κώδικα χαμηλής ποιότητας. Λύση στις παραπάνω προβληματικές περιπτώσεις μπορεί να προσφέρουν οι 43
αναδομήσεις που, με προβλέψιμα και ασφαλή βήματα, μας οδηγούν στην εφαρμογή ενός προτύπου σχεδίασης σε μια μεταγενέστερη φάση του κύκλου ζωής του λογισμικού και σε κώδικα που έχει ήδη αναπτυχθεί. Η αναδόμηση με πρότυπα σχεδίασης, συνδυάζοντας τα οφέλη από την εφαρμογή μιας αναδόμησης με τα πλεονεκτήματα των προτύπων σχεδίασης, μπορεί να οδηγήσει σε σημαντική βελτίωση της ποιότητας ενός υπάρχοντος σχεδίου λογισμικού. Η τεχνική της αναδόμησης με πρότυπα σχεδίασης αναφέρθηκε από τον Fowler [4], όπου στον κατάλογο των 72 αναδομήσεών του (Βλ. και Πίνακα 2 της ενότητας 2.1.4) περιέχονται και αναδομήσεις που οδηγούν στην υιοθέτηση κάποιου σχεδιαστικού προτύπου προκειμένου να επιλυθεί κάποιο σχεδιαστικό πρόβλημα. Πρόκειται για την αναδόμηση «Αντικατάσταση Τύπου με Κατάσταση/Στρατηγική» (Replace Type Code with State/Strategy), η οποία αξιοποιεί τα πρότυπα «Κατάσταση» και «Στρατηγική», την αναδόμηση «Εισαγωγή Null Αντικείμενου» (Introduce Null Object), που στοχεύει στην εισαγωγή του προτύπου «Null Αντικείμενο» και την αναδόμηση με χρήση του προτύπου «Μέθοδος Εργοστάσιο», η οποία και έχει όνομα «Αντικατάσταση Κατασκευαστή με τη Μέθοδο Εργοστάσιο» (Replace Constructor with Factory Method). Η σημαντικότερη συμβολή στον τομέα της αναδόμησης με πρότυπα σχεδίασης είναι, ωστόσο, αυτή του Kerievsky, όπου στο βιβλίο του Refactorings to Patterns [3] αναλύει 27 αναδομήσεις με πρότυπα σχεδίασης. Οι αναδομήσεις αυτές αφορούν σε 19 διαφορετικά πρότυπα σχεδίασης και κατατάσσονται σε τρεις κατηγορίες ανάλογα με το βαθμό υλοποίησης του σχετικού προτύπου. Όπως καταγράφεται και στον Πίνακα 5, η πρώτη κατηγορία αναδομήσεων (κατηγορία to) περιλαμβάνει αναδομήσεις που πρέπει να υλοποιήσουν πλήρως το σχεδιαστικό πρότυπο προκειμένου να επιτευχθεί βελτίωση στον τρόπο σχεδίασης του λογισμικού. Χαρακτηριστικό παράδειγμα τέτοιας αναδόμησης είναι η αναδόμηση «Διαμόρφωση Μεθόδου Υποδείγματος» (Form Template Method). Η συγκεκριμένη αναδόμηση αποσκοπεί στην απομάκρυνση διπλότυπου κώδικα. Προφανώς και δε νοείται να υλοποιηθεί εν μέρει το συγκεκριμένο πρότυπο, διότι σε μια τέτοια περίπτωση δε θα έχει απομακρυνθεί ο διπλότυπος κώδικας. Στη δεύτερη κατηγορία αναδομήσεων, την κατηγορία towards, περιλαμβάνονται αναδομήσεις οι οποίες προσφέρουν σημαντική βελτίωση στον τρόπο σχεδίασης του λογισμικού ακόμα και αν το σχετικό σχεδιαστικό 44
πρότυπο υλοποιηθεί μερικώς. Τέλος, ο σκοπός της εφαρμογής μιας αναδόμησης προς ένα πρότυπο σχεδίασης είναι η βελτίωση της σχεδίασης του λογισμικού. Σε περίπτωση που μετά από την εκτέλεση μιας αναδόμησης δεν παρατηρηθεί βελτίωση στον τρόπο σχεδίασης του λογισμικού ή παρατηρηθεί ότι έπρεπε να εφαρμοστεί κάποιο άλλο σχεδιαστικό πρότυπο, υπάρχουν αναδομήσεις που περιγράφουν πώς μπορείς να απομακρύνεις ένα πρότυπο ή πώς να αντικαταστήσεις ένα πρότυπο με κάποιο άλλο. Αυτές οι αναδομήσεις έχουν καταγραφεί στην κατηγορία away, στην τελευταία στήλη του Πίνακα 5. Τέλος, όπως άλλωστε σε όλες τις αναδομήσεις, έτσι και στην περίπτωση της αναδόμησης της προσανατολισμένης σε πρότυπα σχεδίασης, καθοριστικό ρόλο για την επιλογή της κατάλληλης αναδόμησης προς κάποιο πρότυπο σχεδίασης είναι ο εντοπισμός στον κώδικα του σχεδιαστικού προβλήματος το οποίο απομακρύνεται μέσω της εφαρμογής του προτύπου. Αυτά τα σχεδιαστικά προβλήματα αντιστοιχούν στις κακές οσμές που περιγράψαμε στην ενότητα 2.1.3. Σημαντική βοήθεια προς αυτήν την κατεύθυνση προσφέρει η καταγραφή από τον Kerievsky των 12 πιο συχνών κακών οσμών και των αντίστοιχων αναδομήσεων με πρότυπα σχεδίασης που αποσκοπούν στην απομάκρυνσή τους, η οποία και παρουσιάστηκε στον Πίνακα 1 της ενότητας 2.1.3. Pattern To Towards Away Adapter Extract Adapter, Unify Interfaces with Unify Interfaces with Adapter Adapter Builder Encapsulate Composite Collecting Parameter Command Composed Method Composite Creation with Builder Move Accumulation to Collecting Parameter Replace Conditional Dispatcher with Command Compose Method Replace One/Many Distinctions with Composite, Extract Composite, Replace Implicit Tree with Composite Replace Constructors Replace Conditional Dispatcher with Command Encapsulate Composite with Builder 45
Method Decorator Factory Factory Method Interpreter Iterator Null Object Observer Singleton State Strategy Template Method Visitor with Creation Method Move Embellishment to Decorator Move Creation Knowledge to Factory, Encapsulate Classes with Factory Introduce Polymorphic Creation with Factory Method Replace Implicit Language with Interpreter Introduce Null Object Replace Hard Coded Notifications with Observer Limit Instantiation with Singleton Replace State-Altering Conditionals with State Replace Conditional Logic with Strategy Form Template Method Move Accumulation to Visitor Move Embellishment to Decorator Replace Hard Coded Notifications with Observer Replace State- Altering Conditionals with State Replace Conditional Logic with Strategy Move Accumulation to Visitor Move Accumulation to Visitor Inline Singleton Πίνακας 5: Οι 27 αναδομήσεις με πρότυπα σχεδίασης του Kerievsky και η κατηγοριοποίησή τους 46
Κεφάλαιο 3 - Αυτοματοποίηση Αναδόμησης προς το Πρότυπο Null Object 3.1 Το Πρότυπο Σχεδίασης Null Object Το πρότυπο σχεδίασης Null Object πρωτοεμφανίστηκε στην βιβλιογραφία το 1996 από τον Woolf [30], ενώ στη συνέχεια, το 2003, και ο Henney [31] ασχολήθηκε με την περιγραφή του προτύπου. Σκοπός του προτύπου, όπως αναφέρουν, είναι να «κρύψει» την απουσία ενός αντικειμένου με το να παρέχει ένα άλλο αντικείμενο «αντικαταστάτη» (πρόκειται για το Null Object), το οποίο θα έχει, μεν, την ίδια διεπαφή με το αρχικό αντικείμενο, αλλά δε θα κάνει κάτι ή όπως αναφέρουν θα έχει μια προκαθορισμένη «do nothing» συμπεριφορά. Ο όρος προκαθορισμένη «do nothing» συμπεριφορά αναφέρεται στο γεγονός ότι όλες οι μέθοδοι της Null Object κλάσης υλοποιούνται έτσι ώστε είτε να έχουν κενό σώμα είτε να επιστρέφουν προκαθορισμένα αποτελέσματα (default results). Η χρήση του προτύπου Null Object ενδείκνυται σε περιπτώσεις όπου μια κλάση «πελάτης» έχει μια αναφορά σε ένα αντικείμενο του οποίου η τιμή ενδέχεται να είναι ίση με null. Σε μια τέτοια περίπτωση, απαιτείται να γράφονται έλεγχοι για null πριν οποιαδήποτε χρήση του αντικειμένου. Αν το αποτέλεσμα του ελέγχου σε περίπτωση που το αντικείμενο έχει τιμή ίση με null είναι να μη γίνεται κάτι ή να ανατίθεται μια προκαθορισμένη τιμή, τότε μπορεί να εφαρμοστεί το σχεδιαστικό πρότυπο Null Object, ώστε να μη χρειάζονται πλέον οι έλεγχοι για null και η κλάση «πελάτης» να αντιμετωπίζει με ενιαίο και διαφανή τρόπο τόσο το αντικείμενο με την «αληθινή» συμπεριφορά, όσο και το Null αντικείμενο. Σε μια τέτοια περίπτωση οι μέθοδοι της Null Object κλάσης θα πρέπει να οριστούν ώστε να έχουν την κατάλληλη προκαθορισμένη «do nothing» συμπεριφορά ώστε να είναι δυνατή η απομάκρυνση των ελέγχων για null. Η χρήση του προτύπου Null Object κατά την ανάπτυξη λογισμικού παρέχει αρκετά σημαντικά οφέλη. Καταρχάς, μέσω της εκμετάλλευσης των αρχών της κληρονομικότητας (inheritance) και του πολυμορφισμού (polymorphism), ο κώδικας 47
της κλάσης «πελάτης» απλοποιείται, μιας και μπορούν να απομακρυνθούν οι περιττές και επαναλαμβανόμενες υποθετικές δηλώσεις, οι οποίες συχνά αποτελούν αιτία πρόσθετης πολυπλοκότητας του υπό ανάπτυξη συστήματος λογισμικού. Με αυτόν τον τρόπο ο κώδικας γίνεται αφενός πιο κατανοητός και αφετέρου πιο εύκολα τροποποιήσιμος και συντηρήσιμος. Με την εφαρμογή του προτύπου, η κλάση «πελάτης» δεν χρειάζεται να εισάγει υποθετικές δηλώσεις κάθε φορά που καλεί κάποια συμπεριφορά του αντικειμένου και με αυτόν τον τρόπο το σύστημα λογισμικού καθίσταται πιο εύκολα επεκτάσιμο και λιγότερο επιρρεπές σε σφάλματα. Επιπλέον, και οι υπόλοιπες κλάσεις «πελάτη» του συστήματος, στις οποίες ταιριάζει μια τέτοια συμπεριφορά, μπορούν να επαναχρησιμοποιήσουν το Null αντικείμενο και να επωφεληθούν και αυτές από την εφαρμογή του. Στα μειονεκτήματα της εφαρμογής του προτύπου θα λέγαμε ότι συγκαταλέγεται, καταρχάς, η αύξηση του αριθμού των κλάσεων στο σύστημα. Επιπλέον, με τη χρήση του προτύπου ενδέχεται να επιβαρυνθεί η επικοινωνία μεταξύ της κλάσης «πελάτης» και του αντικειμένου για το οποίο εφαρμόζεται το πρότυπο. Η επιβάρυνση αυτή προκύπτει από το γεγονός ότι, λόγω της κοινής διεπαφής του Null Αντικειμένου με το «Πραγματικό» αντικείμενο, κάθε φορά η κλάση «πελάτης» θα εκτελεί κλήσεις μεθόδων ή θα υπολογίζει τις ενδεχομένως πολύπλοκες τιμές των παραμέτρων αυτών των κλήσεων μεθόδων, ενέργειες που σε διαφορετική περίπτωση δε θα γινόντουσαν. Ένα ακόμα πιθανό μειονέκτημα ενδέχεται να αποτελέσει το γεγονός ότι σε περίπτωση που οι διάφορες κλάσεις «πελάτης» του συστήματος απαιτούν διαφορετική «do nothing» συμπεριφορά, θα πρέπει να δημιουργηθούν παραπάνω από μία Null Object κλάσεις, αυξάνοντας περισσότερο τον αριθμό των νέων κλάσεων στο σύστημα. Επιπλέον, η χρήση του προτύπου μπορεί να κάνει πιο περίπλοκο τον τρόπο σχεδίασης του συστήματος, σε περίπτωση που το λογισμικό περιελάμβανε μόνο λίγους υποθετικούς ελέγχους για null. Τέλος, η συντήρηση της δομής του προτύπου καθίσταται πολύπλοκη μιας και κάθε προσθήκη νέας μεθόδου στην υπερκλάση της Null Object κλάσης απαιτεί να δοθεί η κατάλληλη μέριμνα ώστε να εισαχθεί η μέθοδος και στην Null Object κλάση. Αναφορικά με τη δομή του προτύπου Null Object, στη βιβλιογραφία αναφέρονται 3 διαφορετικές προσεγγίσεις, οι οποίες και παρουσιάζονται στις Εικόνες 2, 3 και 4. Στην προσέγγιση που παρουσιάζεται στην Εικόνα 2, η Null Object κλάση ορίζεται ως 48
υποκλάση της κλάσης του «Πραγματικού» Αντικειμένου και επαναορίζει τις κληρονομούμενες μεθόδους ώστε να παρέχουν την κατάλληλη «do nothing» συμπεριφορά. Η κλάση «πελάτης» έχει μια αναφορά σε ένα αντικείμενο τύπου ActualClass, αλλά στα σημεία του κώδικα που το πεδίο αυτό θα είχε τιμή ίση με null θα του ανατίθεται ένα Null Object αντικείμενο με αποτέλεσμα να μπορούν να απομακρυνθούν οι null υποθετικές δηλώσεις που ελέγχουν την τιμή του πεδίου. Ο κίνδυνος αυτής της υλοποίησης του προτύπου είναι ότι αν μια νέα μέθοδος προστεθεί στην ActualClass, οι προγραμματιστές πρέπει να θυμηθούν να την επαναορίσουν και στην Null Object κλάση με την κατάλληλη do nothing συμπεριφορά, γιατί σε διαφορετική περίπτωση η Null Object κλάση θα κληρονομήσει υλοποίηση που θα οδηγήσει σε μη επιθυμητή συμπεριφορά κατά την εκτέλεση του συστήματος. Η εναλλακτική δομή του προτύπου που φαίνεται στην Εικόνα 3 περιλαμβάνει την αφηρημένη κλάση AbstractClass, η οποία παρέχει την κοινή διεπαφή τόσο για την ActualClass όσο και για την κλάση του Null Object, ενώ παρόμοια είναι η υλοποίηση του προτύπου που παρουσιάζεται στην Εικόνα 4, με μόνη διαφορά ότι οι κλάσεις NullClass και ActualClass υλοποιούν μια κοινή διεπαφή (interface). Εικόνα 2: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας υποκλάσης Εικόνα 3: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας αφηρημένης κλάσης 49
Εικόνα 4: Η δομή του προτύπου Null Object με χρήση της προσέγγισης δημιουργίας διεπαφής Ακολούθως παρατίθεται ένα παράδειγμα μιας περίπτωσης στην οποία βρίσκει εφαρμογή το πρότυπο Null Object [31]. Στο συγκεκριμένο παράδειγμα η κλάση Service έχει ένα πεδίο τύπου Log το οποίο αντιπροσωπεύει μια προαιρετική υπηρεσία καταγραφής (logging). Στην Εικόνα 5 παρουσιάζεται το διάγραμμα κλάσεων του παραδείγματος πριν και μετά την εφαρμογή του προτύπου Null Object, ενώ στην Εικόνα 6 φαίνεται ο κώδικας για κάθε κλάση του παραδείγματος πριν και μετά την εφαρμογή του προτύπου. Όπως φαίνεται και μέσω των Εικόνων 5 και 6, με την εφαρμογή του προτύπου Null Object προστίθεται στην ιεραρχία των κλάσεων η κλάση NullLog, η οποία υλοποιείται με do nothing συμπεριφορά. Επιπλέον, το πεδίο log αρχικοποιείται, μέσω του κατασκευαστή της κλάσης Service, με ένα αντικείμενο τύπου NullLog, οπότε πλέον η συσχέτιση της κλάσης Service με το αντικείμενο Log είναι υποχρεωτική. Τέλος, εκμεταλλευόμενοι την αρχή του πολυμορφισμού μπορούν να απαλειφθούν οι υποθετικές δηλώσεις if που ελέγχουν αν η τιμή του πεδίου είναι διάφορη του null. Εικόνα 5: Το διάγραμμα κλάσεων του παραδείγματος πριν και μετά την εφαρμογή του προτύπου Null Object 50
Εικόνα 6: Ο κώδικας του παραδείγματος πριν και μετά την εφαρμογή του προτύπου Null Object [31] 3.2 Χειρωνακτική Αναδόμηση προς το Πρότυπο Null Object Η κυριότερη συνεισφορά στον τομέα της χειρωνακτικής αναδόμησης προς το πρότυπο Null Object είναι αυτή του Fowler [4] και εν συνεχεία του Kerievsky [3]. O Fowler στο βιβλίο του [4], όπως αναφέρθηκε και στην ενότητα 2.1.4, παρέχει έναν εκτενή κατάλογο 72 αναδομήσεων, για κάθε μία από τις οποίες, εκτός των άλλων, παραθέτει βήμα προς βήμα την περιγραφή του αλγορίθμου που πρέπει να ακολουθηθεί για την εφαρμογή της. Όπως παρουσιάστηκε και στον Πίνακα 2 της ενότητας 2.1.4, η αναδόμηση προς το πρότυπο Null Object είναι μία από τις 51
αναδομήσεις που αναλύει ο Fowler, ενώ την κατατάσσει στις αναδομήσεις που αποσκοπούν στην απλοποίηση υποθετικών εκφράσεων. Κατά τον Fowler, οι περιπτώσεις που ενδείκνυται η εφαρμογή του προτύπου Null Object είναι όταν υπάρχουν επαναλαμβανόμενοι έλεγχοι για null για ένα αντικείμενο, ώστε αναλόγως της τιμής του να εκτελείται κάποια διαφορετική συμπεριφορά. Αντί, λοιπόν, να ελέγχεται η τιμή του αντικειμένου και αναλόγως να καλείται η συμπεριφορά που ταιριάζει, μέσω της δύναμης του πολυμορφισμού μπορεί να καλείται κατευθείαν η συμπεριφορά και το αντικείμενο αναλόγως του υλοποιημένου τύπου του να ενεργεί καταλλήλως. Με άλλα λόγια, με την εφαρμογή του προτύπου Null Object οι εναλλακτικές συμπεριφορές ενός αντικειμένου «κρύβονται» πίσω από μια κοινή διεπαφή και με αυτόν τον τρόπο μπορούν να απομακρυνθούν οι υποθετικές δηλώσεις if που ελέγχουν για null την τιμή του αντικειμένου. Ο Fowler περιγράφει, επιπλέον, και τα βήματα για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object με χρήση της προσέγγισης δημιουργίας υποκλάσης (Βλ. και Εικόνα 2 της ενότητας 3.1) ως ακολούθως: 1. Δημιούργησε μια υποκλάση της source κλάσης του αντικειμένου, η οποία και θα λειτουργεί ως η null εκδοχή της κλάσης του αντικειμένου. Δημιούργησε μια μέθοδο isnull τόσο στην source κλάση όσο και στη null κλάση. Η isnull μέθοδος θα επιστρέφει false στην περίπτωση της source κλάσης και true στην περίπτωση της null κλάσης. 2. Ξανά μεταγλώττισε το πρόγραμμα. 3. Εντόπισε όλα τα σημεία του κώδικα που θα επέστρεφαν τιμή null όταν ζητείτο το source αντικείμενο και αντικατάστησέ τα ώστε να επιστρέφουν ένα null αντικείμενο. 4. Βρες όλα τα σημεία του κώδικα που συγκρίνουν την τιμή του αντικειμένου με την τιμή null και αντικατάστησε την έκφραση ελέγχου τους με την κλήση της μεθόδου isnull και καλούντα (invoker) το αντικείμενο. 5. Ξανά μεταγλώττισε το πρόγραμμα και έλεγξε. 6. Εντόπισε τα σημεία του κώδικα όπου κάποια κλάση πελάτης καλεί μια μέθοδο αν το αντικείμενο δεν είναι ίσο με null και εκτελεί κάποια εναλλακτική 52
συμπεριφορά σε περίπτωση που ισούται με null. 7. Για καθεμία από τις περιπτώσεις του προηγούμενου βήματος επαναόρισε την μέθοδο στην null κλάση ώστε να παρέχει την εναλλακτική συμπεριφορά. 8. Διάγραψε όλες τις υποθετικές δηλώσεις που εφαρμόζουν την εναλλακτική συμπεριφορά. 9. Ξανά μεταγλώττισε το πρόγραμμα και έλεγξε. Επιπρόσθετα, και ο Kerievsky στο βιβλίο του [3] περιγράφει βήμα προς βήμα την εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Όπως αναφέρθηκε στην ενότητα 2.3 η αναδόμηση προς το πρότυπο Null Object είναι μία από τις 27 αναδομήσεις με πρότυπα σχεδίασης που αναλύει ο Kerievsky και όπως καταγράφηκε στον Πίνακα 1 της ενότητας 2.1.3, η εφαρμογή της αποσκοπεί στην απομάκρυνση των «κακών οσμών (bad smells)» «διπλότυπος κώδικας (Duplicated Code)» και «υποθετική πολυπλοκότητα (Conditional Complexity)». Σημαντική, βέβαια, είναι η διαφοροποίηση σε σχέση με τον Fowler στις προϋποθέσεις που θέτει ο Kerievsky για την ανίχνευση των περιπτώσεων που ενδείκνυται η εφαρμογή του προτύπου Null Object. Η προσέγγιση του Kerievsky αναφέρει ότι η εισαγωγή του προτύπου Null Object στοχεύει στην απομάκρυνση της ανάγκης ελέγχου αν ένα πεδίο (field) ή μεταβλητή (variable) είναι null, προτού κληθεί κάποια μέθοδος αυτού. Πιο συγκεκριμένα, αν μια κλάση «πελάτης» καλεί μια μέθοδο ενός πεδίου ή μιας μεταβλητής που είναι null, τότε προκαλείται η ρίψη μιας Null Pointer Exception. Για να αποφευχθεί αυτή η προβληματική κατάσταση και να προστατευτεί το σύστημα από την πρόκληση κάποιας Null Pointer Exception, απαιτείται να γράφονται έλεγχοι για το αν η τιμή του πεδίου ή της μεταβλητής είναι διάφορη του null προτού κληθεί κάποια μέθοδος αυτών. Αυτοί οι επαναλαμβανόμενοι έλεγχοι για null που προστατεύουν από την πρόκληση μιας Null Pointer Exception, επιχειρείται κατά τον Kerievsky να απομακρυνθούν μέσω της εφαρμογής του προτύπου Null Object. Ενώ, λοιπόν, η μεθοδολογία του Fowler επιχειρεί να διαγραφούν υποθετικές δηλώσεις που αφορούν στην εκτέλεση εναλλακτικής συμπεριφοράς αναλόγως του αν το πεδίο ή η μεταβλητή ισούται ή όχι με null, η προσέγγιση του Kerievsky αποσκοπεί στην απομάκρυνση υποθετικών ελέγχων που προστατεύουν από την πρόκληση Null Pointer Exception. Ανεξαρτήτως, βέβαια, της προσέγγισης, η λύση για τη δυνατότητα 53
απομάκρυνσης των εν λόγω υποθετικών δηλώσεων είναι η ανάθεση στο πεδίο ή στη μεταβλητή του σωστού αντικειμένου τη σωστή στιγμή. Πιο συγκεκριμένα, στο πεδίο ή στη μεταβλητή που μπορεί να ισούται με null, ανατίθεται ένα στιγμιότυπο ενός Null Object το οποίο παρέχει μια κενή συμπεριφορά (do-nothing behavior) ή μια προκαθορισμένη συμπεριφορά (default behavior) ή μια συμπεριφορά που δεν επηρεάζει την εκτέλεση του κώδικα (harmless behavior). Σε επόμενο σημείο του κώδικα το πεδίο ή η μεταβλητή μπορεί να οριστεί ως στιγμιότυπο κάποιου άλλου αντικειμένου. Με αυτόν τον τρόπο μπορεί να απομακρυνθεί από τον κώδικα η υποθετική λογική που ελέγχει για null. Και ο Kerievsky με τη σειρά του περιγράφει τα βήματα που πρέπει να ακολουθηθούν προκειμένου να εισαχθεί το πρότυπο Null Object. Συγκεκριμένα, αναφέρει ότι ο αλγόριθμος που περιγράφεται στη συνέχεια μπορεί να ακολουθηθεί προκειμένου να εφαρμοστεί το πρότυπο Null Object, με την προϋπόθεση ότι η ίδια null υποθετική λογική παρουσιάζεται στα διάφορα σημεία του κώδικα επειδή γίνεται αναφορά σε κάποια συμπεριφορά ενός πεδίου ή μια τοπικής μεταβλητής όταν αυτά είναι ακόμη null. Επίσης, αναφέρει ότι το πρότυπο Null Object μπορεί να υλοποιηθεί είτε μέσω της προσέγγισης δημιουργίας υποκλάσης (Βλ. Εικόνα 2 της ενότητας 3.1), είτε μέσω της προσέγγισης υλοποίησης μιας κοινής διεπαφής (Βλ. Εικόνα 4 της ενότητας 3.1). 1. Δημιούργησε μια null κλάση είτε εφαρμόζοντας την αναδόμηση «Εξαγωγής Κλάσης» (Extract Class) στην κλάση πηγή (source class) και ορίζοντας την null κλάση ως υποκλάση της κλάσης πηγή, είτε ορίζοντας την null κλάση να υλοποιεί τη διεπαφή που υλοποιεί η κλάση πηγή. Σε περίπτωση που αυτή η διεπαφή δεν υπάρχει ήδη, δημιούργησέ τη εφαρμόζοντας την αναδόμηση «Εξαγωγής Διεπαφής» (Extract Interface) στην κλάση πηγή. Σημειώνεται ότι ο όρος κλάση πηγή αναφέρεται στην κλάση που επιθυμούμε να προστατευτεί από nulls. Ξανά μεταγλώττισε το πρόγραμμα. 2. Εντόπισε στην κλάση πελάτη κώδικα υποθετικής λογικής που ελέγχει για null την τιμή ενός στιγμιότυπου της κλάσης πηγή και καλεί κάποια μέθοδο αυτού σε περίπτωση που δεν είναι ίσο με null ή υλοποιεί κάποια εναλλακτική συμπεριφορά σε περίπτωση που ισούται με null. Όρισε τη συγκεκριμένη 54
μέθοδο στη null κλάση ώστε να υλοποιεί την εναλλακτική συμπεριφορά. Ξανά μεταγλώττισε το πρόγραμμα. 3. Επανάλαβε το βήμα 2 για τους υπόλοιπους υποθετικούς ελέγχους που σχετίζονται με την κλάση πηγή. 4. Εντόπισε μια κλάση που έχει μία ή περισσότερες εμφανίσεις υποθετικών ελέγχων για null και αρχικοποίησε το πεδίο ή την τοπική μεταβλητή που αναφέρεται σε αυτούς τους ελέγχους με ένα στιγμιότυπο της null κλάσης. Εφάρμοσε αυτήν την αρχικοποίηση όσο το δυνατό νωρίτερα στον κύκλο ζωής του πεδίου ή της μεταβλητής, για παράδειγμα κατά τη δήλωσή του. Αυτή η αρχικοποίηση δεν πρέπει να επηρεάζει τυχόν προϋπάρχοντα κώδικα που αναθέτει στο πεδίο ή στη μεταβλητή ένα στιγμιότυπο της κλάσης πηγή, αλλά πρέπει να γίνεται πριν οποιαδήποτε άλλη ανάθεση. Ξανά μεταγλώττισε το πρόγραμμα. 5. Απομάκρυνε από την κλάση του βήματος 4 κάθε υποθετική λογική που ελέγχει για null την τιμή του εν λόγω πεδίου ή μεταβλητής. Ξανά μεταγλώττισε και έλεγξε το πρόγραμμα. 6. Επανάλαβε τα βήματα 4 και 5 για κάθε κλάση που έχει μία ή περισσότερες εμφανίσεις υποθετικών ελέγχων για null. Ενώ οι παραπάνω αλγόριθμοι του Fowler και του Kerievsky προσπαθούν να περιγράψουν αναλυτικά την εφαρμογή της αναδόμησης προς το πρότυπο Null Object, δεν παύουν να είναι αρκετά γενικοί και ως εκ τούτου ελλιπείς και μη εφαρμόσιμοι σε διάφορες περιπτώσεις. Μέσα από τη μελέτη τους διαφαίνεται ότι δε θέτουν τις απαραίτητες προϋποθέσεις ώστε η εφαρμογή της αναδόμησης να μην εισάγει σφάλματα και να εξασφαλίζει τη διατήρηση της συμπεριφοράς του λογισμικού. Αυτές τις ελλείψεις προσπάθησε να λάβει υπόψη και να καλύψει η προτεινόμενη μεθοδολογία αναδόμησης που αναπτύχθηκε στα πλαίσια της παρούσας διπλωματικής εργασίας και η οποία θα παρουσιαστεί με λεπτομέρειες στο επόμενο κεφάλαιο. Στη συνέχεια του παρόντος κεφαλαίου θα γίνει μια επισκόπηση της βιβλιογραφίας που σχετίζεται με την αυτοματοποιημένη αναδόμηση προς το πρότυπο σχεδίασης Null Object, ενώ θα περιγραφούν οι μέθοδοι και τεχνικές ανάλυσης κώδικα στην 55
αυτοματοποιημένη αναδόμηση που χρησιμοποιήθηκαν στην παρούσα εργασία. 3.3 Προσεγγίσεις Αυτόματης Αναδόμησης με Χρήση του Προτύπου Σχεδίασης Null Object Η αξία και η σημασία της αυτοματοποιημένης αναδόμησης καταδεικνύεται πολύ έντονα αν αναλογιστεί κανείς το συνεχώς αυξανόμενο μέγεθος των σύγχρονων συστημάτων λογισμικού. Το μεγάλο μέγεθος και η υψηλή πολυπλοκότητα των συστημάτων λογισμικού καθιστά τη χειρωνακτική αναδόμηση μια επίπονη διαδικασία, με υψηλές απαιτήσεις τόσο από πλευράς χρόνου, όσο και από πλευράς κόστους και απαιτούμενης προσπάθειας. Επιπλέον, η χειρωνακτική αναδόμηση, χωρίς την υποστήριξη από κάποιο εργαλείο, είναι μια διαδικασία επιρρεπής στην εισαγωγή σφαλμάτων και στην τροποποίηση της συμπεριφοράς του συστήματος λογισμικού. Τέλος, η διαδικασία της ανίχνευσης προβληματικών σημείων στον κώδικα που χρήζουν κάποιας αναδόμησης εφόσον γίνεται μόνο από τον ανθρώπινο παράγοντα, εξαρτάται από την προσωπική κρίση και γνώση του μηχανικού λογισμικού που τη διενεργεί, θέτοντας σοβαρά θέματα αξιοπιστίας της προτεινόμενης διαδικασίας αναδόμησης. Για όλους τους προαναφερθέντες λόγους η αυτοματοποίηση της αναδόμησης έχει απασχολήσει σοβαρά τη βιομηχανία λογισμικού και αρκετά εργαλεία που επιτελούν αυτόν το σκοπό έχουν δημιουργηθεί, όπως παρουσιάστηκε και στην ενότητα 2.1.5. Ενώ αρκετές απλές αναδομήσεις έχουν αυτοματοποιηθεί, η έρευνα στον τομέα της αυτοματοποίησης της αναδόμησης προς κάποιο πρότυπο σχεδίασης βρίσκεται σε σχετικά πρώιμο στάδιο. Το γεγονός αυτό οφείλεται αφενός στην εννοιολογική φύση των προτύπων σχεδίασης που καθιστά δύσκολο τον εντοπισμό περιπτώσεων εφαρμογής τους και αφετέρου στη διαφορετική φύση κάθε προτύπου, η οποία και καθιστά δυσχερή την ανάπτυξη μιας γενικής μεθοδολογίας που θα καλύπτει όλα τα διαφορετικά είδη αναδομήσεων προς πρότυπα σχεδίασης. Αναφορικά με την αυτοματοποίηση της αναδόμησης προς το πρότυπο Null Object, η βιβλιογραφία χαρακτηρίζεται από έλλειψη τεχνικών αυτοματοποίησης τόσο της διαδικασίας ανίχνευσης των προβληματικών σημείων στον πηγαίο κώδικα, όσο και της εκτέλεσης του μετασχηματισμού της αναδόμησης προκειμένου να εφαρμοστεί το σχεδιαστικό πρότυπο. Αυτήν τη στιγμή, εξ' όσων γνωρίζουμε, δεν υπάρχουν έτοιμα εργαλεία για την αυτόματη αναδόμηση προς το πρότυπο Null Object. Υπάρχουν 56
κάποιες προσπάθειες αυτόματης αναδόμησης σε παρεμφερή πρότυπα σχεδίασης, όπως η αυτοματοποίηση της αναδόμησης «Αντικατάσταση Υποθετικής Λογικής με Πολυμορφισμό» (Replace Conditional with Polymorphism) τόσο από τους Τσάνταλη και Χατζηγεωργίου [33], όσο και από τον Frank [34], αλλά και η αυτοματοποιημένη αναδόμηση προς τα πρότυπα κατάσταση και στρατηγική (Replace Type Code with State/Strategy) από τους Τσάνταλη και Χατζηγεωργίου [35] και από τη Χριστοπούλου στη συνέχεια [36]. Συγκεκριμένα, όμως, για το σχεδιαστικό πρότυπο Null Object δε συναντάται κάποια σχετική προσπάθεια στη βιβλιογραφία. Επιπλέον, υπάρχουν διάφορα εργαλεία στατικής ανάλυσης του κώδικα, όπως το PMD [37] και το FindBugs [38], τα οποία προσφέρουν τη δυνατότητα ανίχνευσης σημείων όπου υπάρχει κίνδυνος να δημιουργηθεί Null Pointer Exception, όπου η εφαρμογή της αναδόμησης προς το πρότυπο Null Object θα έλυνε αυτά τα προβλήματα, χωρίς όμως να προτείνουν κάποια αναδόμηση. Την έλλειψη στον τομέα της αυτόματης αναδόμησης προς το σχεδιαστικό πρότυπο Null Object επιχειρεί να καλύψει η παρούσα διπλωματική εργασία. Η προσέγγιση που αναπτύχθηκε αποσκοπεί τόσο στην αυτοματοποιημένη ανίχνευση των προβληματικών σημείων στον κώδικα που ενδείκνυται η εφαρμογή του προτύπου, όσο και στην αυτόματη εκτέλεση της αναδόμησης. Ιδιαίτερη προσπάθεια καταβλήθηκε ώστε η προτεινόμενη μεθοδολογία να είναι αρκετά γενική ώστε να εντοπίζει όσο το δυνατόν περισσότερες υποψήφιες περιπτώσεις για την εφαρμογή του προτύπου, αλλά και να εξασφαλίζει την απουσία σφαλμάτων και τη διατήρηση της συμπεριφοράς του συστήματος κατά την εφαρμογή της αναδόμησης. Αναλυτική παρουσίαση της προτεινόμενης μεθοδολογίας γίνεται στο επόμενο κεφάλαιο 4, ενώ στην ενότητα που ακολουθεί περιγράφονται οι τεχνικές ανάλυσης κώδικα που χρησιμοποιηθήκαν στη μεθοδολογία αυτοματοποιημένης αναδόμησης προς το πρότυπο Null Object που αναπτύχθηκε. 3.4 Μέθοδοι Ανάλυσης Κώδικα στην Αυτοματοποιημένη Αναδόμηση Η μεθοδολογία αυτόματης ανίχνευσης και αναδόμησης περιπτώσεων στις οποίες ενδείκνυται το σχεδιαστικό πρότυπο Null Object που αναπτύχθηκε στα πλαίσια της παρούσας διπλωματικής εργασίας, βασίζεται στην τεχνική της στατικής ανάλυσης του πηγαίου κώδικα (static analysis). Η τεχνική της στατικής ανάλυσης εξετάζει τον πηγαίο κώδικα σε χρόνο μεταγλώττισης, χωρίς να τον εκτελεί με συγκεκριμένα 57
δεδομένα. Μέσω της τεχνικής της στατικής ανάλυσης αντλούνται πλήθος πληροφοριών τόσο για τη δομή του προγράμματος (τις κλάσεις, τις μεθόδους, τις μεταβλητές, κτλ.), όσο και για τον έλεγχο ροής του, οι οποίες πληροφορίες αντιστοιχούν σε ιδιότητες του πηγαίου κώδικα που ισχύουν σε κάθε πιθανή εκτέλεση του και δεν εξαρτώνται από συγκεκριμένα δεδομένα εισόδου. Προκειμένου να επιτευχθεί ο σκοπός της παρούσας εργασίας, η μεθοδολογία αναδόμησης που αναπτύχθηκε βασίστηκε στην τεχνική της στατικής ανάλυσης του υπό εξέταση κώδικα ώστε να παραχθεί ως κύρια μορφή αναπαράστασης του πηγαίου κώδικα το Αφηρημένο Συντακτικό Δέντρο (Abstract Syntax Tree, AST). Ο εντοπισμός των υποψηφίων προτάσεων αναδόμησης προς το πρότυπο Null Object βασίστηκε στην εξερεύνηση του αφηρημένου συντακτικού δέντρου και επιπροσθέτως, όταν οι απαιτούμενες πληροφορίες δεν ήταν δυνατόν να εξαχθούν από αυτό, χρησιμοποιήθηκε τόσο ο Γράφος Ροής Ελέγχου (Control Flow Graph, CFG) όσο και ο Γράφος Εξαρτήσεων Προγράμματος (Program Dependence Graph, PDG). Στις ενότητες που ακολουθούν θα γίνει μια αναλυτική παρουσίαση καθεμιάς από τις προαναφερθείσες μορφές αναπαράστασης του πηγαίου κώδικα που χρησιμοποιήθηκαν στα πλαίσια της προσέγγισης που αναπτύχθηκε. 3.4.1 Αφηρημένα Συντακτικά Δέντρα (AST) Τα αφηρημένα συντακτικά δέντρα είναι από τις πιο δημοφιλείς αναπαραστάσεις πηγαίου κώδικα, οι οποίες χρησιμοποιούνται κατά κόρον κατά την ανάλυση κώδικα από τους μεταγλωττιστές (compilers). Τα αφηρημένα συντακτικά δέντρα αναπαριστούν τον πηγαίο κώδικα με ένα δέντρο. Η δεντρική αναπαράσταση του πηγαίου κώδικα είναι «αφηρημένη» υπό την έννοια ότι δεν απεικονίζονται κάποια συγκεκριμένα συστατικά του πηγαίου κώδικα, όπως π.χ. οι παρενθέσεις μια έκφρασης ελέγχου, τα σχόλια, τα κενά και οι κενές γραμμές. Κάθε συστατικό του πηγαίου κώδικα αντιστοιχίζεται σε έναν κόμβο στο αφηρημένο συντακτικό δέντρο. Πιο συγκεκριμένα, ένα αφηρημένο συντακτικό δέντρο αποτελείται από ένα σύνολο εσωτερικών κόμβων, οι οποίοι αναπαριστούν τα μη τερματικά σύμβολα του πηγαίου κώδικα, όπως π.χ. τις δηλώσεις μεταβλητών, τις δηλώσεις μεθόδων, τις εντολές ανάθεσης, κτλ. και από ένα σύνολο κόμβων φύλλα, οι οποίοι αντιστοιχούν στα τερματικά σύμβολα του κώδικα, όπως π.χ. στους τελεστές, στις συμβολοσειρές, στις αριθμητικές τιμές, κτλ.. Καθένας από αυτούς τους κόμβους έχει έναν τύπο και ένα 58
προκαθορισμένο σύνολο ιδιοτήτων που αφορούν στο συστατικό του κώδικα που αναπαρίσταται και συμφωνούν με τη γραμματική της εκάστοτε γλώσσας προγραμματισμού [39]. Στην Εικόνα 7 παρουσιάζεται ένα παράδειγμα μιας απλής Java κλάσης και στην Εικόνα 8 απεικονίζεται μέρος του αφηρημένου συντακτικού δέντρου της κλάσης, όπως παράγεται από το ASTViewer plug-in του Eclipse [40]. Στη μεθοδολογία που αναπτύχθηκε στα πλαίσια της παρούσας εργασίας, παράγεται το AST δέντρο του υπό εξέταση συστήματος λογισμικού και διασχίζεται προκειμένου να εντοπιστούν οι περιπτώσεις που ικανοποιούν τα κριτήρια που ορίζονται από τη μεθοδολογία ώστε να εφαρμοστεί η αναδόμηση προς το πρότυπο Null Object. Στη συνέχεια o χρήστης δύναται να επιθεωρήσει τις προτάσεις αναδόμησης και να επιλέξει εκείνες που επιθυμεί να εκτελέσει. Κατά την εφαρμογή της επιλεγμένης από το χρήστη πρότασης αναδόμησης, το AST δέντρο του υπό εξέταση κώδικα ανασυντάσσεται και παρέχεται στο χρήστη η δυνατότητα προεπισκόπησης της προτεινόμενης αναδόμησης προτού επιλέξει την εφαρμογή της, καθώς και η δυνατότητα απόρριψής της. Όταν ο χρήστης επιβεβαιώσει την εφαρμογή της προτεινόμενης αναδόμησης, οι μετασχηματισμοί του AST δέντρου χρησιμοποιούνται ώστε να τροποποιηθεί ο πηγαίος κώδικας του λογισμικού χωρίς την παρέμβαση του χρήστη. Ως σημαντικά μειονεκτήματα των AST δέντρων ως μορφές αναπαράστασης θα μπορούσαμε να αναφέρουμε τις αυξημένες απαιτήσεις σε μνήμη για την αποθήκευση τους εξαιτίας του όγκου των πληροφοριών που αποθηκεύουν, τον αυξημένο χρόνο για τη δημιουργία τους, αλλά και τη δυσκολία στη διάσχιση τους για την εξαγωγή πληροφοριών απ αυτά. Εικόνα 7: Παράδειγμα κλάσης Hello.java 59
Εικόνα 8: Απόσπασμα του αφηρημένου συντακτικού δέντρου της κλάσης Hello.java της Εικόνας 7, όπως παράγεται από το ASTViewer plug-in του Eclipse 60
3.4.2 Γράφος Ροής Ελέγχου (CFG) Η δεύτερη κύρια μορφή αναπαράστασης της δομής του πηγαίου κώδικα που παράγεται από τα εργαλεία στατικής ανάλυσης είναι ο γράφος ροής ελέγχου (control flow graph) [41], [42]. Ο γράφος ροής ελέγχου είναι ένας κατευθυνόμενος γράφος στον οποίο κάθε κόμβος αναπαριστά κάθε βασικό συστατικό (εντολή) του πηγαίου κώδικα και κάθε ακμή αναπαριστά τη ροή ελέγχου μεταξύ των κόμβων. Πολύ συχνή μορφή του γράφου ροής ελέγχου είναι αυτή στην οποία κάθε κόμβος του αναπαριστά μία βασική ενότητα (basic block) του προγράμματος και οι ακμές του απεικονίζουν τη ροή ελέγχου μεταξύ των βασικών ενοτήτων. Ως βασική ενότητα ορίζεται η μέγιστη ακολουθία από εντολές στην οποία η ροή ελέγχου εισέρχεται μόνο από την αρχή της (δηλαδή την πρώτη εντολή) και εξέρχεται μόνο από το τέλος της (δηλαδή την τελευταία εντολή). Σε μια βασική ενότητα απαγορεύεται η ύπαρξη ενδιάμεσων αλμάτων, δηλαδή αν εκτελεστεί η πρώτη της εντολή τότε θα πρέπει να εκτελεστούν όλες οι υπόλοιπες. Κάθε βασική ενότητα αποτελείται από έναν leader κόμβο, ο οποίος θεωρείται είτε ο πρώτος κόμβος του τμήματος κώδικα που εξετάζεται, είτε ένας κόμβος συγχώνευσης (join node), είτε ένας κόμβος που ακολουθεί έναν κόμβο διακλάδωσης (branch node). Ένας γράφος ροής ελέγχου απεικονίζει όλα τα πιθανά μονοπάτια εκτέλεσης του κώδικα και αξιοποιείται τόσο στη βελτιστοποίηση μεταγλωττιστών, όσο και στον έλεγχο λογισμικού, π.χ. στην τεχνική ελέγχου της συμβολικής εκτέλεσης και στον έλεγχο κάλυψης μονοπατιών (path coverage) από μία σουίτα δοκιμασιών ελέγχου (test suite). Στην παρούσα διπλωματική εργασία ο γράφος ροής ελέγχου χρησιμοποιείται ώστε να ελεγχθεί η ικανοποιησιμότητα των προϋποθέσεων της προτεινόμενης μεθοδολογίας και συγκεκριμένα ώστε να επιβεβαιωθεί ότι μια συγκεκριμένη εντολή βρίσκεται σε όλα τα μονοπάτια εκτέλεσης του κώδικα. Στην Εικόνα 9 που ακολουθεί παρατίθεται ένα τμήμα κώδικα μαζί με το χωρισμό του σε 7 βασικές ενότητες (Β 1,, Β 7 ), ενώ στην Εικόνα 10 παρουσιάζεται ο γράφος ροής ελέγχου (αριστερά) και ο γράφος ροής ελέγχου με βασικές ενότητες (δεξιά) για το συγκεκριμένο τμήμα κώδικα [41]. Η αρίθμηση των κόμβων των γράφων αντιστοιχεί στην αρίθμηση των εντολών του τμήματος κώδικα της Εικόνας 9. 61
Εικόνα 9: Τμήμα κώδικα χωρισμένο σε βασικές ενότητες [41] Εικόνα 10: Γράφος ροής ελέγχου (αριστερά) και γράφος ροής ελέγχου με βασικές ενότητες (δεξιά) για το τμήμα κώδικα της Εικόνας 9 [41] 62
3.4.3 Γράφος Εξαρτήσεων Προγράμματος (PDG) Μια ακόμα σημαντική αναπαράσταση πηγαίου κώδικα είναι ο γράφος εξαρτήσεων προγράμματος (program dependence graph, PDG), ο οποίος αναπαριστά τόσο τις εξαρτήσεις ελέγχου όσο και τις εξαρτήσεις δεδομένων ενός προγράμματος. Ο γράφος εξαρτήσεων προγράμματος αποτελείται από κόμβους καθένας από τους οποίους απεικονίζει μια εντολή ή μια συνθήκη ελέγχου του προγράμματος και από ακμές οι οποίες υποδηλώνουν τις εξαρτήσεις μεταξύ των κόμβων. Τα δύο είδη ακμών εξάρτησης που περιλαμβάνει ο γράφος εξαρτήσεων προγράμματος είναι [43]: H ακμή εξάρτησης ελέγχου (control dependence edge): η οποία αναπαριστά τη συνθήκη ελέγχου από την οποία εξαρτάται η εκτέλεση μιας εντολής του προγράμματος. Μια ακμή εξάρτησης ελέγχου από τον κόμβο p στον κόμβο q υποδηλώνει ότι η εκτέλεση της εντολής του κόμβου q εξαρτάται από την εκτέλεση της εντολής του κόμβου p. Η ακμή εξάρτησης δεδομένων (data dependence edge): η οποία υποδηλώνει ότι οι κόμβοι οι οποίοι ενώνονται χρησιμοποιούν την ίδια μεταβλητή. Μια ακμή εξάρτησης δεδομένων από τον κόμβο p στον κόμβο q υποδηλώνει ότι ο υπολογισμός που αφορά ο κόμβος q θα έχει λανθασμένη τιμή αν η σειρά εκτέλεσης των κόμβων p και q αντιστραφεί. Ένας πιο τυπικός ορισμός θα ήταν ο ακόλουθος: Έστω ότι σε έναν PDG γράφο υπάρχουν οι κόμβοι p και q. Θεωρείται ότι υπάρχει μια εξάρτηση δεδομένων από τον p στον q αναφορικά με τη μεταβλητή v αν και μόνο αν υπάρχει ένα μη κενό μονοπάτι από τον κόμβο p στον κόμβο q στο οποίο δε μεσολαβεί δήλωση της μεταβλητής v και είτε ο κόμβος p περιέχει εντολή δήλωσης της μεταβλητής v και ο κόμβος q εντολή που χρησιμοποιεί τη μεταβλητή v, είτε ο κόμβος p περιέχει εντολή που χρησιμοποιεί τη μεταβλητή v και ο κόμβος q περιέχει εντολή δήλωσης της μεταβλητής v, είτε τόσο ο κόμβος p όσο και ο κόμβος q περιέχουν εντολή δήλωσης της μεταβλητής v. Στην Εικόνα 11 αναπαρίσταται ο γράφος εξαρτήσεων κλάσεων (class dependence graph, ClDG) για το τμήμα κώδικα της Εικόνας 9. Στο γράφο αυτόν αναπαρίσταται ο γράφος εξαρτήσεων προγράμματος για τη μέθοδο statement της κλάσης Customer, στον οποίον και περιέχεται τόσο ο υπογράφος με τις εξαρτήσεις ελέγχου όσο και ο υπογράφος με τις εξαρτήσεις δεδομένων [41]. 63
Πρέπει να αναφερθεί ότι ένα σημαντικό πλεονέκτημα του γράφου εξαρτήσεων προγράμματος σε σχέση με το αφηρημένο συντακτικό δέντρο είναι ότι αναπαριστά την πραγματική ροή των δεδομένων και οι εντολές αποτυπώνονται με τη σειρά που εκτελούνται και όχι με τη σειρά με την οποία εμφανίζονται στον πηγαίο κώδικα. Στην παρούσα διπλωματική εργασία ο γράφος εξαρτήσεων προγράμματος χρησιμοποιήθηκε κατά την εφαρμογή της τεχνικής slicing προκειμένου να ελεγχθεί η ισχύς των προϋποθέσεων της προτεινόμενης μεθοδολογίας. Στην επόμενη ενότητα αναλύεται η τεχνική slicing. Εικόνα 11: Ο γράφος εξαρτήσεων κλάσεων για το τμήμα κώδικα της Εικόνας 9, στον οποίο περιέχεται τόσο ο υπογράφος με τις εξαρτήσεις ελέγχου όσο και ο υπογράφος με τις εξαρτήσεις δεδομένων [41] 64
3.4.4 Τεχνική Slicing Η τεχνική του τεμαχισμού προγράμματος (program slicing) εφαρμόζεται ώστε ένα συγκεκριμένο τμήμα κώδικα ενός προγράμματος να χωριστεί σε μικρότερα αυτόνομα τμήματα, τα επονομαζόμενα τεμάχια (slices), με τρόπο ώστε το πρόγραμμα να διατηρεί την ίδια συμπεριφορά [44]. Ως τεμάχιο (slice) ορίζεται ένα σύνολο εντολών ενός προγράμματος οι οποίες ενδέχεται να επηρεάζουν την τιμή μιας μεταβλητής x σε ένα συγκεκριμένο σημείο ενδιαφέροντος p. Το ζεύγος (p, x) ονομάζεται κριτήριο τεμαχισμού (slicing criterion) [44]. Σε γενικές γραμμές ένα τεμάχιο υπολογίζεται βρίσκοντας, βάσει των εξαρτήσεων ελέγχου και δεδομένων, τις εντολές που άμεσα ή έμμεσα επηρεάζουν την τιμή της μεταβλητής που ορίζεται στο κριτήριο τεμαχισμού. Στη βιβλιογραφία έχουν παρουσιαστεί πολλά είδη τεχνικών τεμαχισμού προγράμματος. Αναλόγως του αν για τον τεμαχισμό χρησιμοποιούνται πληροφορίες που προκύπτουν σε χρόνο εκτέλεσης ή όχι, ο τεμαχισμός διακρίνεται σε στατικό (static slicing) και δυναμικό (dynamic slicing). Στο στατικό τεμαχισμό ο υπολογισμός των τεμαχίων γίνεται με χρήση των στατικά διαθέσιμων πληροφοριών, ενώ στο δυναμικό [45] χρησιμοποιούνται σαν είσοδος οι τιμές των μεταβλητών όπως προέκυψαν από μια συγκεκριμένη εκτέλεση του προγράμματος, έτσι ώστε τα τεμάχια να υπολογιστούν με μεγαλύτερη ακρίβεια. Επιπλέον, όπως αναφέρει ο Τσάνταλης [46], ο τεμαχισμός ανάλογα με την κατεύθυνση στην οποία εφαρμόζεται διακρίνεται σε ευθύ (forward slicing), σύμφωνα με τον οποίον ένα τεμάχιο περιέχει όλες τις εντολές και τα κατηγορήματα ελέγχου (control predicates) που ενδέχεται να επηρεαστούν από μια μεταβλητή σε ένα συγκεκριμένο σημείο του κώδικα και σε ανάστροφο (backward slicing), σύμφωνα με τον οποίον ένα τεμάχιο περιέχει όλες τις εντολές και τα κατηγορήματα ελέγχου που ενδέχεται να επηρεάσουν μια μεταβλητή σε ένα δεδομένο σημείο του κώδικα. Ενώ, αναλόγως της εμβέλειάς του ο τεμαχισμός διακρίνεται σε ενδο-διαδικαστικό (intraprocedural), ο οποίος υπολογίζει ένα τεμάχιο στα πλαίσια μιας διαδικασίας και σε δια-διαδικαστικό (interprocedural), ο οποίος παράγει τεμάχια που εκτείνονται εκτός των ορίων μιας διαδικασίας. Η τεχνική του τεμαχισμού προγράμματος βρίσκει εφαρμογή σε πολλούς τομείς της τεχνολογίας λογισμικού, όπως στην αποσφαλμάτωση (debugging), στον έλεγχο λογισμικού (testing), στην μέτρηση της συνεκτικότητας (cohesion measurement) και στο reverse engineering. Στον τομέα της αναδόμησης μια πολύ δημοφιλής χρήση της 65
τεχνικής του τεμαχισμού είναι κατά την εφαρμογή της αναδόμησης εξαγωγής μεθόδου (extract method). Ο Maruyama [41] πρότεινε τη χρήση της τεχνικής του τεμαχισμού βάσει μπλοκ (block-based slicing) προκειμένου να αναδομηθεί με αυτοματοποιημένο τρόπο μια μέθοδος χωρίς να αλλάξει η παρατηρήσιμη συμπεριφορά της. Την ίδια τεχνική χρησιμοποίησε και επέκτεινε και ο Τσάνταλης κατά την αυτοματοποίηση της αναδόμησης εξαγωγής μεθόδου [46], αλλά και αξιοποίησε και η παρούσα διπλωματική εργασία κατά την αυτοματοποίηση της αναδόμησης προς το πρότυπο Null Object (Βλ. και ενότητα 4.3.4.1). Πιο συγκεκριμένα, η τεχνική του τεμαχισμού βάσει μπλοκ σύμφωνα με τον Maruyama [41] δεν εξάγει τα τεμάχια από όλο το πρόγραμμα αλλά από μια περιοχή που αποτελείται από διαδοχικές βασικές ενότητες (basic blocks). Η προσέγγιση του παίρνει ως είσοδο ένα κριτήριο τεμαχισμού (n, u) το οποίο περιλαμβάνει μία εντολή n που ανήκει στη μέθοδο m και μία μεταβλητή u που ορίζεται ή χρησιμοποιείται στην εντολή n. Καταρχάς, δημιουργείται ο γράφος ροής ελέγχου (CFG) της μεθόδου m, έτσι ώστε να χωριστεί σε βασικές ενότητες, με τον τρόπο που παρουσιάστηκε στην ενότητα 3.4.2. Υπενθυμίζεται ότι ως βασική ενότητα ορίζεται μια αλληλουχία διαδοχικών εντολών στις οποίες η ροή του ελέγχου ξεκινάει από την αρχή και σταματάει στο τέλος χωρίς να σταματάει ενδιάμεσα ή να διακλαδίζεται. Κάποιοι κόμβοι του CFG χαρακτηρίζονται ως leaders κόμβοι και σε αυτούς περιλαμβάνονται είτε ο πρώτος κόμβος της μεθόδου, είτε ένας κόμβος συγχώνευσης, είτε ένας κόμβος που βρίσκεται ακριβώς μετά από έναν κόμβο διακλάδωσης. Για κάθε leader κόμβο, η βασική ενότητά του αποτελείται από τον συγκεκριμένο leader κόμβο και από όλους τους ακόλουθους κόμβους μέχρι τον επόμενο leader κόμβο ή τον τελευταίο κόμβο του CFG. Σε επόμενο βήμα δημιουργείται ο γράφος εξαρτήσεων προγράμματος (PDG) για τη μέθοδο m, ο οποίος περιέχει τόσο τις εξαρτήσεις ελέγχου όσο και τις εξαρτήσεις δεδομένων μεταξύ των εντολών της μεθόδου m. Στη συνέχεια υπολογίζεται το σύνολο των boundary blocks Blocks(n) για την εντολή n του κριτηρίου τεμαχισμού. Δεδομένου ότι η εντολή n ανήκει στη βασική ενότητα B, το σύνολο των boundary blocks είναι αυτό που προκύπτει από την τομή των επόμενων μπλοκ με τα οποία συνδέεται το μπλοκ B (forward reachable blocks) και των dominated blocks του κόμβου r. Ο κόμβος r είναι ο κόμβος που άμεσα κυριαρχεί (dominate) στον leader κόμβο της βασικής ενότητας B. Μια βασική ενότητα 66
θεωρείται ότι κυριαρχείται (dominated) από τον κόμβο r αν υπάρχει μια εξάρτηση ελέγχου (control dependence) που μεταβαίνει από τον κόμβο r στη συγκεκριμένη βασική ενότητα. Για την εύρεση τόσο του κόμβου r, όσο και των βασικών ενοτήτων που είναι dominated από τον κόμβο r χρησιμοποιείται ο block-based γράφος εξαρτήσεων ελέγχου (block-based control dependence graph) της μεθόδου m, ο οποίος προκύπτει από τον αντίστοιχο γράφο εξαρτήσεων ελέγχου που περιέχεται στον PDG, και στον οποίον οι κόμβοι που βρίσκονται μέσα στην ίδια βασική ενότητα αντιστοιχίζονται σε έναν block κόμβο και διαγράφονται οι ακμές μεταξύ των κόμβων της ίδιας βασικής ενότητας. Στη συνέχεια για κάθε boundary block B n Blocks(n) δημιουργείται ένα υπογράφος του PDG ο οποίος αποτελείται μόνο από τους κόμβους που ανήκουν στην block-based περιοχή του B n. Ως block-based περιοχή R(B n ) ενός boundary block Β n θεωρείται το σύνολο των κόμβων που βρίσκονται σε επόμενες ιεραρχικά βασικές ενότητες (forward reachable blocks) από το Β n. Το block-based τεμάχιο S B (n, u, B n ) για το κριτήριο τεμαχισμού (n, u) και το boundary block B n, αντιστοιχεί στο σύνολο των εντολών που ενδέχεται να επηρεάσουν τον υπολογισμό της μεταβλητής u στην εντολή n, όπως αυτές προκύπτουν από τον PDG που αντιστοιχεί στην block-based περιοχή R(B n ). Στην Εικόνα 12 απεικονίζεται ο block-based γράφος εξαρτήσεων ελέγχου (blockbased control dependence graph) της μεθόδου statement που παρουσιάστηκε στην Εικόνα 9 της ενότητας 3.4.2, ενώ στην Εικόνα 13 παρουσιάζονται οι βασικές ενότητες της μεθόδου, οι γραμμοσκιασμένοι leader κόμβοι τους, καθώς και τα boundary blocks Blocks(10) = {Β 1, Β 2, Β 3, Β 4 } για την εντολή της γραμμής 10. Παρατηρούμε ότι το τεμάχιο S B (10, renderpoints, B 1 ) για το κριτήριο τεμαχισμού (10, renderpoints) και το boundary block B 1 αντιστοιχεί στο σύνολο των εντολών {3, 4, 6, 7, 9, 10, 11} [41]. 67
Εικόνα 12: Ο block-based γράφος εξαρτήσεων ελέγχου (block-based control dependence graph) της μεθόδου statement που παρουσιάστηκε στην Εικόνα 9 [41] Εικόνα 13: Οι βασικές ενότητες της μεθόδου της Εικόνας 9, οι γραμμοσκιασμένοι leader κόμβοι τους, καθώς και τα boundary blocks για την εντολή της γραμμής 10 [41] 68
Κεφάλαιο 4 Προτεινόμενη Μεθοδολογία Η αναδόμηση του πηγαίου κώδικα προς το πρότυπο Null Object αποτελεί ένα ερευνητικό θέμα στον τομέα της αναδόμησης που έχει απασχολήσει αρκετούς ερευνητές, όπως τους Fowler [4] και Kerievsky [3], ενώ και οι Woolf [30] και Henney [31] έχουν περιγράψει πώς μπορεί να πραγματοποιηθεί η εφαρμογή του προτύπου Null Object με απώτερο σκοπό τη βελτίωση της ποιότητας του τρόπου σχεδίασης ενός συστήματος λογισμικού. Όπως αναλύθηκε εκτενώς στο Κεφάλαιο 3, το πρότυπο Null Object, βασιζόμενο στις αρχές της κληρονομικότητας και του πολυμορφισμού, ενδείκνυται να χρησιμοποιηθεί στις περιπτώσεις όπου υπάρχουν στον κώδικα επαναλαμβανόμενοι έλεγχοι αν ένα αντικείμενο (object) είναι παρόν (διάφορο του null) προτού σταλεί ένα μήνυμα σε αυτό. Με άλλα λόγια, βασιζόμενοι στην αρχή του πολυμορφισμού, μπορούμε αντί να ελέγχουμε κάθε φορά την τιμή ενός αντικειμένου και μόνο αν βρεθεί διάφορη του null να εκτελούμε κάποια συμπεριφορά του, να εκτελούμε κατευθείαν τη συμπεριφορά του. Το αντικείμενο αναλόγως του υλοποιημένου τύπου του θα ενεργήσει καταλλήλως. Στις περιπτώσεις, όμως, που είτε λόγω άγνοιας του προτύπου είτε λόγω εσφαλμένης υλοποίησης, απουσιάζει το πρότυπο, στον κώδικα θα υπάρχουν επαναλαμβανόμενοι έλεγχοι για null προτού κληθεί κάποια μέθοδος του αντικειμένου, ώστε να αποφευχθεί η πρόκληση μιας Null Pointer Exception. Οι επαναλαμβανόμενοι αυτοί έλεγχοι για null μας θυμίζουν τις ευρέως διαδεδομένες «κακές οσμές (bad smells)» «διπλότυπος κώδικας (Duplicated Code)» και «υποθετική πολυπλοκότητα (Conditional Complexity)» που αναφέρει ο Kerievsky [3] (Βλ. και Πίνακα 1 της ενότητας 2.1.3). Όντως, σύμφωνα με τον Kerievsky, αν έχεις υποθετική λογική που καθορίζει τη συμπεριφορά ενός αντικειμένου όταν είναι null και η συγκεκριμένη null λογική επαναλαμβάνεται στον κώδικα του συστήματος, μπορείς να εφαρμόσεις το πρότυπο Null Object και έτσι να μειώσεις το διπλότυπο υποθετικό κώδικα και να απλοποιήσεις τον τρόπο σχεδίασης του συστήματος λογισμικού. Προς αυτήν την κατεύθυνση στοχεύει και η μεθοδολογία που αναπτύχθηκε στα πλαίσια της παρούσας 69
διπλωματικής εργασίας. Προτού παρουσιαστεί η προτεινόμενη μεθοδολογία για την αυτοματοποιημένη ανίχνευση και εφαρμογή περιπτώσεων αναδόμησης προς το πρότυπο Null Object, κρίνεται αναγκαίο να αναλυθούν περαιτέρω τα προβλήματα από την ύπαρξη υποθετικής λογικής και διπλότυπου κώδικα σε ένα σύστημα λογισμικού. 4.1 Τα Προβλήματα της Χρήσης Υποθετικής Λογικής και Διπλότυπου Κώδικα Η υποθετική δήλωση, αναμφίβολα, είναι από τις πιο συχνά χρησιμοποιούμενες δομές σε ένα σύστημα λογισμικού. Ενώ η χρήση υποθετικής λογικής (conditional logic) μπορεί να θεωρηθεί αθώα όταν αυτή είναι απλή, εύκολα κατανοητή και εκτείνεται σε λίγες γραμμές κώδικα, τις περισσότερες φορές αυτό δεν ισχύει. Οι υποθετικές δηλώσεις συνήθως είναι μεγάλες σε έκταση, πολύπλοκες και περικλείουν πολύ λειτουργικότητα. Για αυτόν το λόγο, η υποθετική λογική συχνά χαρακτηρίζεται ως προβληματική, καθώς εισάγει πρόσθετη πολυπλοκότητα, μειώνοντας την κατανοησιμότητα και τη συντηρησιμότητα μιας μονάδας λογισμικού. Οι μονάδες λογισμικού που βασίζονται σε υποθετικές δομές είναι πιο επιρρεπείς στην εισαγωγή σφαλμάτων κατά τη φάση της συντήρησης και πιο δύσκολα μπορούν να πραγματοποιηθούν σε αυτές επεκτάσεις και τροποποιήσεις της λειτουργικότητάς τους. Επιπλέον, η εκτεταμένη χρήση υποθετικής λογικής αυξάνει την προσπάθεια που απαιτείται και τη δυσκολία για τον έλεγχο μιας μονάδας λογισμικού, μιας και αυξάνεται ο αριθμός των δοκιμασιών ελέγχου (tests) που πρέπει να διενεργηθούν ώστε να εξασφαλιστεί ότι όλες οι εντολές του προγράμματος έχουν εκτελεστεί τουλάχιστον μια φορά. Αρκετοί προγραμματιστές πολλές φορές χρησιμοποιούν υποθετικές δηλώσεις σε περιπτώσεις όπου θα μπορούσε να χρησιμοποιηθεί ο πολυμορφισμός. Ως εκ τούτου, η αυξημένη χρήση υποθετικής λογικής συναντάται πολύ τακτικά στα συστήματα λογισμικού, οδηγώντας επιπλέον, στην επανάληψη κώδικα (code duplication), μιας και τα τμήματα των υποθετικών δηλώσεων βρίσκονται διασκορπισμένα σε διάφορα σημείου του κώδικα του συστήματος. Τα δύο παραπάνω επακόλουθα, όπως προαναφέρθηκε, μας φέρνουν άμεσα στο μυαλό τις «κακές οσμές» «διπλότυπος κώδικας (Duplicated Code)» και «υποθετική πολυπλοκότητα (Conditional Complexity)» που αναφέρει ο Kerievsky [3]. Αυτές οι 70
κακές οσμές αποτελούν ενδείξεις προβληματικών σημείων στον κώδικα που χρήζουν αναδόμησης. Βέβαια, αναλόγως της μορφής της υποθετικής δήλωσης, προτείνεται η χρήση διαφορετικής αντιμετώπισης και η εφαρμογή διαφορετικού τύπου αναδόμησης προκειμένου να μειωθεί η πολυπλοκότητα του συστήματος λογισμικού. Πιο συγκεκριμένα, όπως παρουσιάστηκε και στον Πίνακα 1 της ενότητας 2.1.3, ο Kerievsky [3] προτείνει για την απλοποίηση του διπλότυπου κώδικα την εφαρμογή επτά διαφορετικών αναδομήσεων. Προφανώς το ποια αναδόμηση ενδείκνυται κάθε φορά καθορίζεται τόσο από τη δομή της υποθετικής δήλωσης όσο και από το σκοπό τον οποίο επιτελεί. Αναφορικά με την εισαγωγή του null αντικειμένου (Introduce Null Object), ο Kerievsky αναφέρει ότι η χρήση της συγκεκριμένης αναδόμησης ενδείκνυται για την απαλοιφή του διπλότυπου κώδικα που σχετίζεται με την επανάληψη υποθετικών ελέγχων που αφορούν στον έλεγχο αν η τιμή ενός αντικειμένου ισούται ή όχι με null. Επίσης, ο Kerievsky συσχετίζει την απλοποίηση πολύπλοκης υποθετικής λογικής με τέσσερις διαφορετικές αναδομήσεις, όπως παρουσιάστηκε και στον Πίνακα 1 της ενότητας 2.1.3. Και αυτή η κακή οσμή μπορεί να απαλειφθεί εισάγοντας το πρότυπο Null Object. Σύμφωνα με τον Kerievsky, η χρήση του αντικειμένου null αντικαθιστά την υποθετική λογική, σε περιπτώσεις όπου ο έλεγχος για το κατά πόσο μια μεταβλητή έχει την τιμή null επαναλαμβάνεται συχνά σε διάφορα σημεία του συστήματος. Η μεθοδολογία που αναπτύχθηκε στα πλαίσια της παρούσας εργασίας και αναλύεται διεξοδικά στις επόμενες ενότητες του κεφαλαίου, βασίστηκε στην προσέγγιση του Kerievsky [3] σχετικά με το πότε ενδείκνυται η εισαγωγή του προτύπου Null Object. Η συγκεκριμένη προσέγγιση, όπως παρουσιάστηκε και στην ενότητα 3.2, αναφέρει ότι η εισαγωγή του προτύπου Null Object στοχεύει στην απομάκρυνση της ανάγκης ελέγχου αν ένα πεδίο (field) ή μεταβλητή (variable) είναι null, προτού κληθεί κάποια μέθοδος αυτού. Οι επαναλαμβανόμενοι έλεγχοι για null που προστατεύουν από την πρόκληση μιας Null Pointer Exception, μπορούν να διαγραφούν με το να καταστήσουμε δυνατή την ασφαλή κλήση της συμπεριφοράς του πεδίου ή της μεταβλητής. Η λύση για να επιτευχθεί αυτό είναι η ανάθεση στο πεδίο ή στη μεταβλητή του σωστού αντικειμένου τη σωστή στιγμή. Όταν ένα πεδίο ή μεταβλητή μπορεί να είναι null, του αναθέτεις ένα στιγμιότυπο ενός Null Object το οποίο 71
παρέχει μια κενή συμπεριφορά (do-nothing behavior) ή μια προκαθορισμένη συμπεριφορά (default behavior) ή μια συμπεριφορά που δεν επηρεάζει την εκτέλεση του κώδικα (harmless behavior). Σε επόμενο σημείο του κώδικα το πεδίο ή η μεταβλητή μπορεί να οριστεί ως στιγμιότυπο κάποιου άλλου αντικειμένου. Με αυτόν τον τρόπο μπορεί να απομακρυνθεί από τον κώδικα η υποθετική λογική που ελέγχει για null. Έχοντας ως αναφορά την παραπάνω προσέγγιση, η προτεινόμενη μεθοδολογία περιορίζεται στην ανίχνευση και απομάκρυνση υποθετικής λογικής που ελέγχει αν ένα πεδίο είναι ίσο ή όχι με null προτού καλέσει κάποια μέθοδο αυτού, ώστε να αποφευχθεί η πρόκληση Null Pointer Exception, μιας και o προγραμματιστής δεν είναι σίγουρος αν στο πεδίο έχει τεθεί τιμή διάφορη του null. Οι έλεγχοι που αφορούν σε τοπικές μεταβλητές δεν εξετάζονται, διότι σε μια τέτοια περίπτωση η εισαγωγή του προτύπου Null Object θα είχε μικρό όφελος μιας και θα απομάκρυνε υποθετικές δηλώσεις μόνο εντός μιας μεθόδου, ενώ θα έπρεπε να επαναλαμβάνεται σε κάθε μέθοδο οδηγώντας σε έκρηξη δημιουργίας νέων κλάσεων. Οι έλεγχοι που αφορούν σε παραμέτρους μιας μεθόδου και πάλι δεν εξετάζονται, διότι η εισαγωγή του προτύπου θα απαιτούσε εκτεταμένες και ενδεχομένως αδύνατες τροποποιήσεις στις κλάσεις πελάτη της μεθόδου, πράγμα που δε συνάδει με το επιδιωκόμενο όφελος από την εισαγωγή του προτύπου. 4.2 Στόχος της Προτεινόμενης Μεθοδολογίας Σκοπός της μεθοδολογίας που αναπτύχθηκε είναι η αυτοματοποιημένη ανίχνευση περιπτώσεων που είναι δυνατή η εφαρμογή του προτύπου Null Object. Επιπλέον, η προτεινόμενη προσέγγιση επιτρέπει, για τις περιπτώσεις που πληρούν τα κριτήρια αυτόματης εφαρμογής της αναδόμησης, και την αυτοματοποιημένη ανασύνταξη του πηγαίου κώδικα ώστε να εφαρμοστεί η δομή του προτύπου. Για την υλοποίηση των ανωτέρω στόχων, η ανάπτυξη της προτεινόμενης μεθοδολογίας βασίστηκε στον υπάρχοντα κώδικα υλοποίησης των αναδομήσεων που επιλύουν τα σχεδιαστικά προβλήματα που υποστηρίζει το JDeodorant [11], ο οποίος και επεκτάθηκε καταλλήλως. Ως εκ τούτου, η προτεινόμενη μεθοδολογία αναπτύχθηκε έχοντας ως αναφορά και πεδίο εφαρμογής το υποσύνολο των προγραμμάτων λογισμικού που έχουν αναπτυχθεί στη γλώσσα προγραμματισμού 72
Java. Πιο συγκεκριμένα, η ανίχνευση των υποψήφιων περιπτώσεων για την εφαρμογή του προτύπου Null Object εκτελείται με τη χρήση στατικής ανάλυσης (static analysis) του πηγαίου κώδικα, προκειμένου να εντοπιστούν τα κατάλληλα πεδία κάθε κλάσης, καθώς και οι κατάλληλες υποθετικές δηλώσεις, που ικανοποιούν τις προϋποθέσεις που είναι απαραίτητες προκειμένου να χαρακτηριστεί ένα πεδίο ως υποψήφιο για την αυτόματη εφαρμογή του προτύπου. Η προτεινόμενη μεθοδολογία, επιπλέον, εντοπίζει και τις περιπτώσεις εκείνες που είναι μεν κατάλληλες για την εφαρμογή του προτύπου Null Object, αλλά δεν είναι δυνατή η αυτοματοποιημένη εφαρμογή του προτύπου μέσω του εργαλείου και η εφαρμογή της αναδόμησης απαιτεί την παρέμβαση του προγραμματιστή. Τα πεδία αυτά χαρακτηρίζονται ως suggested refactorable fields προς το πρότυπο Null Object. Η προσέγγιση που ακολουθήθηκε ουσιαστικά επιτυγχάνεται μέσω της συντακτικής ανάλυσης του υπό εξέταση κώδικα, της παραγωγής του αντίστοιχου αφηρημένου δέντρου του υπό εξέταση συστήματος και της διάσχισής του προκειμένου να εντοπιστούν τα πεδία και οι υποθετικές δομές που ικανοποιούν τα κριτήρια που ορίζονται από τη μεθοδολογία. Η προτεινόμενη μεθοδολογία χρησιμοποιεί ως μορφές στατικής αναπαράστασης του πηγαίου κώδικα εκτός από το δέντρο AST και τους γράφους CFG και PDG. Επιπλέον, αναφορικά με την εφαρμογή της αναδόμησης, η διαδικασία αυτή αποτελείται από μετασχηματισμούς του παραγόμενου AST δέντρου του υπό εξέταση κώδικα και όχι από μετασχηματισμούς που εφαρμόζονται απευθείας στον πηγαίο κώδικα. Με αυτόν τον τρόπο παρέχεται στο χρήστη η δυνατότητα της προεπισκόπησης της προτεινόμενης αναδόμησης προτού επιλέξει την εφαρμογή της, καθώς και η δυνατότητα απόρριψης μιας προτεινόμενης αναδόμησης. Στις επόμενες ενότητες αναλύονται διεξοδικά οι προϋποθέσεις που πρέπει να πληρούνται ώστε μια περίπτωση να ανιχνευτεί ως κατάλληλη για την εφαρμογή του προτύπου Null Object. Επιπλέον, αναλύεται διεξοδικά η διαδικασία ανίχνευσης των υποψήφιων περιπτώσεων αναδόμησης προς το πρότυπο Null Object που προτείνει η μεθοδολογία που αναπτύχθηκε. Κατόπιν, παρουσιάζεται ο τρόπος αυτοματοποιημένης εφαρμογής της αναδόμησης, καθώς και μια λεπτομερής 73
παρουσίαση της υλοποίησης της προτεινόμενης μεθοδολογίας. Το κεφάλαιο ολοκληρώνεται με μια αναφορά στους περιορισμούς και στις αδυναμίες της προτεινόμενης μεθοδολογίας. Κατά τη διάρκεια της ανάλυσης που θα ακολουθήσει, θα χρησιμοποιηθεί σαν αναφορά ένα απλοϊκό παράδειγμα κώδικα προκειμένου να γίνει πιο εύκολα αντιληπτός τόσο ο αλγόριθμος ανίχνευσης όσο και ο αλγόριθμος αυτοματοποιημένης εφαρμογής της αναδόμησης προς το πρότυπο Null Object. Το παράδειγμα περιλαμβάνει την κλάση Room η οποία έχει αναφορά σε ένα πεδίο τύπου Customer (Βλ. και Εικόνα 14). Ένα δωμάτιο δεν είναι απαραίτητο να έχει κάποιον πελάτη, συνεπώς η συσχέτιση μεταξύ της κλάσης Room και Customer δεν είναι υποχρεωτική, όπως φαίνεται και στο διάγραμμα κλάσεων της Εικόνας 15. 74
75
Εικόνα 14: Κώδικας του παραδείγματος Room Customer που θα χρησιμοποιηθεί στην ανάλυση της προτεινόμενης μεθοδολογίας Εικόνα 15: Διάγραμμα κλάσεων του παραδείγματος Room Customer 76
4.3 Ανίχνευση Περιπτώσεων Αναδόμησης Προκειμένου να οριοθετηθεί το πρόβλημα της ανίχνευσης των περιπτώσεων αναδόμησης προς το πρότυπο Null Object, η προτεινόμενη μεθοδολογία ανίχνευσης, αρχικά προσπαθεί να εντοπίσει σε κάθε κλάση του υπό εξέταση κώδικα εκείνα τα πεδία της που είναι τύπου κάποιας κλάσης του υπό εξέταση project ώστε να είναι δυνατή η εφαρμογή της αναδόμησης. Τα πεδία αυτά στη συνέχεια εξετάζονται για να εξακριβωθεί αν ικανοποιούν ένα σύνολο επιλεγμένων προϋποθέσεων που υποδηλώνουν ότι υπάρχει μεγάλη πιθανότητα στην κλάση στην οποία ορίζονται (έστω Context κλάση) να υπάρχουν υποθετικές εκφράσεις που ελέγχουν αν η τιμή τους ισούται ή όχι με null. Οι προαναφερθείσες προϋποθέσεις αναφέρουν ότι η ανίχνευση των περιπτώσεων που απουσιάζει το σχεδιαστικό πρότυπο Null Object, ανάγεται στο πρόβλημα της ανίχνευσης των πεδίων μιας κλάσης τα οποία, καταρχάς, δεν αρχικοποιούνται κατά τη δήλωσή τους ή αρχικοποιούνται σε null. Επιπλέον, ένα πεδίο θα θεωρηθεί υποψήφιο για την εφαρμογή του προτύπου μόνο αν ενδέχεται να μην έχει πάρει τιμή διάφορη του null κατά τη δημιουργία ενός αντικειμένου της Context κλάσης ή και κατά τη «διάρκεια ζωής» αυτού του αντικειμένου. Απαραίτητη, λοιπόν, προϋπόθεση προκειμένου να θεωρηθεί ένα πεδίο ως υποψήφιο για την εφαρμογή του προτύπου Null Object είναι να έχει με τέτοιον τρόπο δηλωθεί μέσα στην Context κλάση ώστε ο προγραμματιστής να μην είναι σίγουρος αν η τιμή του ισούται με null ή όχι. Αυτό με αρκετή βεβαιότητα μας οδηγεί στο συμπέρασμα ότι στην Context κλάση θα υπάρχουν υποθετικές δηλώσεις που θα ελέγχουν αν η τιμή του πεδίου είναι ίση ή διάφορη του null κάθε φορά που αυτό χρησιμοποιείται για την κλήση κάποιας μεθόδου του. Αυτές οι δομές if θα πρέπει στη συνέχεια σε ένα επόμενο στάδιο να εξεταστούν για το αν πληρούν ένα επιλεγμένο σύνολο κριτηρίων ώστε να αποφανθούμε αν είναι δυνατή η διαγραφή τους. Μόνο τα υποψήφια πεδία για τα οποία θα ανιχνευτούν υποθετικές δηλώσεις υποψήφιες για διαγραφή, θα θεωρηθούν τελικά κατάλληλα για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object, μιας και μόνο τότε θα έχουμε όφελος από τη χρήση του προτύπου. Επιπλέον, ένα επιπρόσθετο σύνολο κριτηρίων εξετάζεται ώστε να αποφανθούμε αν 77
για τις υποψήφιες περιπτώσεις πεδίων δύναται να αυτοματοποιηθεί η εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Σε διαφορετική περίπτωση, η προτεινόμενη μεθοδολογία θα κατατάσσει αυτό το πεδίο ως suggested refactorable, μιας και απαιτείται η επέμβαση του προγραμματιστή για την εφαρμογή της αναδόμησης. Όλα τα προαναφερθέντα κριτήρια και προϋποθέσεις επεξηγούνται αναλυτικά στη συνέχεια. 4.3.1 Εντοπισμός των Υποψήφιων Πεδίων Ένα πεδίο θεωρείται υποψήφιο για την εφαρμογή του προτύπου αν δεν αρχικοποιείται ή αρχικοποιείται σε null και είναι τύπου κάποιας κλάσης του υπό εξέταση project (έστω της κλάσης Component). Επιπλέον, εξετάζονται μόνο τα πεδία τα οποία δεν τίθενται σε τουλάχιστον έναν κατασκευαστή (constructor) της Context κλάσης στην οποία ανήκουν. Η προϋπόθεση αυτή υιοθετήθηκε προκειμένου να οριοθετηθεί το πρόβλημα της ανίχνευσης υποψήφιων πεδίων και να είναι πιο στοχευμένη η διαδικασία ανίχνευσης, μιας και για τα πεδία που την ικανοποιούν, ο προγραμματιστής δεν είναι σίγουρος αν θα έχουν πάρει τιμή διάφορη του null και συνεπώς θα γράφει υποθετικές δηλώσεις με έλεγχο για null κάθε φορά που θέλει να καλέσει κάποια συμπεριφορά τους. Αναφορικά με το πότε ένα πεδίο τίθεται σε έναν κατασκευαστή, αυτό θεωρείται ότι ισχύει αν στο σώμα του κατασκευαστή υπάρχει εντολή ανάθεσης (assignment statement) με αριστερό μέλος το πεδίο και δεξί οτιδήποτε διαφορετικό από το null literal (Βλ. Εικόνα 16). Ή εναλλακτικά, ένα πεδίο θεωρείται ότι τίθεται σε έναν κατασκευαστή, αν υπάρχει κλήση της setter μεθόδου του πεδίου με όρισμα (argument) διαφορετικό του null. Μια μέθοδος θεωρείται setter ενός πεδίου, είτε αν έχει όνομα της μορφής set{όνομα_του_πεδίου}, μη διακρίνοντας μεταξύ πεζών ή κεφαλαίων χαρακτήρων, και η δήλωσή της αποτελείται από μία παράμετρο ίδιου τύπου με αυτού του πεδίου (Βλ. Εικόνα 17), είτε αν αποτελείται από μία παράμετρο ίδιου τύπου με αυτού του πεδίου και στο σώμα της υπάρχει εντολή ανάθεσης με δεξί μέλος τη συγκεκριμένη παράμετρο και αριστερό το πεδίο (Βλ. Εικόνα 18). Οι δύο παραπάνω εναλλακτικοί τρόποι εντοπισμού ύπαρξης setter μεθόδου για ένα πεδίο επιλέχθηκαν μιας και επιτρέπουν την ανίχνευση όλων των πιο συχνά εμφανιζόμενων 78
μορφών setter μεθόδου για ένα πεδίο. Πρέπει να διευκρινιστεί ότι σε περίπτωση που σε κάποιον κατασκευαστή καλείται κάποια μέθοδος της Context κλάσης που δεν είναι setter μέθοδος κάποιου άλλου πεδίου της Context κλάσης, τότε θεωρείται ότι το πεδίο που εξετάζουμε τίθεται στον κατασκευαστή χωρίς να το εξετάζουμε περαιτέρω (Βλ. παράδειγμα στην Εικόνα 19). Η υιοθέτηση αυτής της θεώρησης κρίθηκε λογική διότι ένας εξαντλητικός έλεγχος όλων των καλούμενων μεθόδων μπορεί να οδηγούσε σε αλυσιδωτούς ελέγχους πολλών μεθόδων μέχρι να εντοπιστεί αν τίθεται το πεδίο, με αποτέλεσμα την επικίνδυνη αύξηση των απαιτήσεων σε μνήμη και του χρόνου εκτέλεσης της διαδικασίας ανίχνευσης. Σε περίπτωση που κάποιο πεδίο τίθεται σε όλους τους κατασκευαστές της Context κλάσης, θα θεωρηθεί υποψήφιο μόνο αν υπάρχει setter μέθοδος για αυτό. Η προϋπόθεση αυτή και πάλι επιβεβαιώνει την ύπαρξη αβεβαιότητας κατά πόσο το πεδίο έχει πάρει τιμή διάφορη του null, διότι αφενός μεν στο πεδίο ενδέχεται να έχει τεθεί τιμή διάφορη του null μέσω κάποιου κατασκευαστή, αλλά εν συνεχεία μέσω της κλήσης της setter μεθόδου μπορεί να έχει πάρει τιμή ίση με null. Η αβεβαιότητα αυτή αναμένουμε να έχει οδηγήσει τον προγραμματιστή στην εισαγωγή null ελέγχων κάθε φορά που θέλει να καλέσει κάποια συμπεριφορά του πεδίου. Στις Εικόνες 16, 17, 18 και 19 φαίνονται τέσσερις διαφορετικές περιπτώσεις κατασκευαστών της κλάσης Room στις οποίες το πεδίο customer θεωρείται ότι τίθεται. Στην περίπτωση της Εικόνας 16 το πεδίο customer τίθεται λόγω ύπαρξης εντολής ανάθεσης με δεξί μέλος οτιδήποτε διαφορετικό από το null literal. Στην Εικόνα 17 το πεδίο θεωρείται ότι τίθεται στο συγκεκριμένο κατασκευαστή λόγω κλήσης της πρώτης μορφής setter μεθόδου του πεδίου με όρισμα διαφορετικό του null literal. Στο παράδειγμα κατασκευαστή της Εικόνας 18, το πεδίο τίθεται μιας και καλείται η δεύτερης μορφής setter μέθοδος του πεδίου με όρισμα διαφορετικό του null literal. Τέλος, στο παράδειγμα της Εικόνας 19 παρουσιάζεται η περίπτωση ενός κατασκευαστή που θεωρείται ότι θέτει το πεδίο customer διότι καλείται κάποια μέθοδος της κλάσης Room που δεν είναι setter μέθοδος κάποιου άλλου πεδίου της κλάσης Room. 79
Εικόνα 16: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω ύπαρξης εντολής ανάθεσης Εικόνα 17: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω κλήσης setter μεθόδου πρώτης μορφής Εικόνα 18: Περίπτωση κατασκευαστή που θέτει το πεδίο λόγω κλήσης setter μεθόδου δεύτερης μορφής Εικόνα 19: Περίπτωση κατασκευαστή που θεωρείται ότι θέτει το πεδίο λόγω κλήσης μεθόδου της context κλάσης Room που δεν είναι setter μέθοδος κάποιου άλλου πεδίου της κλάσης Room 80
Βάσει των προϋποθέσεων που περιγράφηκαν στη συγκεκριμένη ενότητα, για το παράδειγμα Room Customer της Εικόνας 14 συμπεραίνουμε ότι η context κλάση Room έχει ένα πεδίο με όνομα customer το οποίο δεν αρχικοποιείται κατά τη δήλωσή του στην κλάση Room και επιπλέον, είναι τύπου Customer που αντιστοιχεί σε μία κλάση του υπό εξέταση project. Επίσης, η κλάση Room έχει έναν και μόνο κατασκευαστή στον οποίον το πεδίο customer δεν τίθεται, συνεπώς, το πεδίο customer είναι μέχρι στιγμής υποψήφιο για την εφαρμογή του προτύπου Null Object. 4.3.2 Εντοπισμός της Δυνατότητας Αυτόματης Εφαρμογής της Αναδόμησης Τα πεδία που από τη μέχρι τώρα ανάλυση έχουν χαρακτηριστεί ως υποψήφια για την εφαρμογή του προτύπου Null Object εξετάζονται για να αποφανθούμε αν ανήκουν σε κάποια από τις περιπτώσεις που δεν επιτρέπουν την αυτοματοποιημένη εφαρμογή της αναδόμησης. Έστω ότι στην κλάση Context έχει εντοπιστεί ως υποψήφιο για την εφαρμογή του προτύπου Null Object το πεδίο component που είναι τύπου Component. Στην Εικόνα 20 παρουσιάζεται το διάγραμμα κλάσεων της δομής του κώδικα πριν και μετά την εφαρμογή της αναδόμησης. Εικόνα 20: Διάγραμμα κλάσεων της μορφής του κώδικα πριν και μετά την εφαρμογή της αναδόμησης Οι περιπτώσεις στις οποίες δεν επιτρέπεται η αυτοματοποιημένη εφαρμογή της αναδόμησης είναι οι ακόλουθες: 1. Αν η κλάση Component που αντιστοιχεί στον τύπο του πεδίου είναι υποκλάση κάποιας άλλης κλάσης. Σε μια τέτοια περίπτωση και δεδομένου 81
ότι στη γλώσσα προγραμματισμού Java δεν επιτρέπεται η πολλαπλή κληρονομικότητα, δεν είναι δυνατή η δημιουργία της ιεραρχίας που απαιτείται κατά τη φάση της αναδόμησης και συνεπώς είναι απαραίτητη η παρέμβαση του προγραμματιστή προκειμένου να εφαρμοστεί η δομή του προτύπου. 2. Αν ο τύπος του πεδίου είναι interface. Κατά τη φάση της εφαρμογής της αναδόμησης θα πρέπει να προστεθούν στην κλάση Component δύο νέες μέθοδοι, όπως θα αναλυθεί διεξοδικά στην επόμενη ενότητα 4.4. Η αλλαγή αυτή, όμως, στην περίπτωση ενός interface δημιουργεί προβλήματα στις κλάσεις του project που το υλοποιούν, μιας και θα πρέπει να υλοποιούν και τις δυο νέες μεθόδους. Η τροποποίηση όλων των κλάσεων που υλοποιούν το συγκεκριμένο interface ξεφεύγει από τα πλαίσια της εφαρμογής του προτύπου και για αυτό οι περιπτώσεις αυτές θα χαρακτηριστούν ως προτεινόμενες για την εφαρμογή του προτύπου. 3. Αν η κλάση Component της οποίας είναι τύπου το πεδίο έχει μη private πεδία τα οποία προσπελαύνονται μέσα στην Context κλάση μέσω του πεδίου. Κατά την εφαρμογή της αναδόμησης αποφασίστηκε τα μη private πεδία της κλάσης Component να μην μεταφερθούν ένα βήμα πιο πάνω στην ιεραρχία, δηλαδή στην αφηρημένη υπερκλάση της (AbstractComponent), η οποία και δημιουργείται προκειμένου να εφαρμοστεί το πρότυπο Null Object (Βλ. ενότητα 4.4). Η απόφαση αυτή βασίστηκε στο γεγονός ότι κάτι τέτοιο θα καθιστούσε δυσχερή την αρχικοποίηση των πεδίων στην αφηρημένη κλάση AbstractComponent σε περίπτωση που αυτά αρχικοποιούνταν για παράδειγμα μέσω της κλήσης μιας μεθόδου της κλάσης Component. Μια τέτοια αρχικοποίηση θα ήταν δυνατή μόνο αν γίνονταν εκτεταμένες αλλαγές στην κλάση Component και το σώμα των εν λόγω μεθόδων «ανέβαινε» στην αφηρημένη υπερκλάση της. Εν κατακλείδι, η απόφαση να μη μεταφέρονται τα πεδία της κλάσης Component στην αφηρημένη υπερκλάση της, δημιουργεί λάθη σε περίπτωση που αυτά είναι μη private και προσπελαύνονται μέσα στην Context κλάση μέσω του πεδίου component, μιας και το πεδίο αυτό ορίζεται κατά την εφαρμογή του προτύπου ως τύπου AbstractComponent. Για αυτόν το λόγο αυτές οι περιπτώσεις πεδίων θεωρούνται από τη μεθοδολογία ως μη υποψήφιες για την αυτοματοποιημένη εφαρμογή του προτύπου Null Object. 82
4. Αν η κλάση Component της οποίας είναι τύπου το πεδίο είναι αφηρημένη (abstract) κλάση η οποία υλοποιεί κάποιο interface. Αν η κλάση Component του τύπου του πεδίου είναι αφηρημένη και παράλληλα υλοποιεί κάποιο interface, νομιμοποιείται να μην υλοποιεί κάποιες μεθόδους αυτού του interface. Αυτό, όμως, δημιουργεί πρόβλημα στην κλάση NullComponent που δημιουργείται κατά την εφαρμογή της δομής του προτύπου, διότι αυτή δεν ορίζεται ως αφηρημένη και ως εκ τούτου θα πρέπει να υλοποιεί και αυτές τις μεθόδους που δεν υλοποιούσε η κλάση Component. Μια λύση στο πρόβλημα θα ήταν να εντοπίζονται ποιες είναι οι μέθοδοι του interface που δεν υλοποιούνται στην κλάση Component και να υλοποιούνται με κάποιο dummy τρόπο στην κλάση NullComponent. Μια τέτοια ανάλυση και υλοποίηση, όμως, ξεφεύγει από το σκοπό του προτύπου και καθιστά δυσχερή και πολύπλοκη τη διαδικασία της αυτοματοποιημένης εφαρμογής της αναδόμησης, και συνεπώς αυτές οι περιπτώσεις πεδίων θεωρούνται ως περιπτώσεις που δεν επιτρέπουν την αυτοματοποιημένη εφαρμογή της αναδόμησης 5. Αν το πεδίο component είναι μη private και η Context κλάση στην οποία ορίζεται έχει κάποια υποκλάση. Αν η Context κλάση έχει κάποια υποκλάση τότε αυτή θα κληρονομεί όλα τα μη private πεδία αυτής, άρα και το πεδίο component σε περίπτωση που είναι μη private. Ο τύπος του πεδίου αυτού κατά την εφαρμογή της αναδόμησης αλλάζει σε AbstractComponent. Η αλλαγή αυτή, όμως, είναι πολύ πιθανό να δημιουργήσει σύγκρουση τύπων στην υποκλάση της Context κλάσης οπουδήποτε χρησιμοποιείται αυτό το πεδίο, π.χ. σαν argument σε κλήση μεθόδων ή σαν δεξί μέλος σε εντολές ανάθεσης. Για το λόγο αυτό και η παραπάνω περίπτωση κατατάσσεται σε αυτές που απαιτούν την επέμβαση του προγραμματιστή προκειμένου να εφαρμοστεί η δομή του προτύπου Null Object. 4.3.3 Εντοπισμός Υποθετικών Δηλώσεων Υποψήφιων για Διαγραφή Για καθένα πεδίο που από τη μέχρι τώρα ανάλυση έχει χαρακτηριστεί ως υποψήφιο για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object, εξετάζεται κατά πόσο υπάρχουν υποθετικές δηλώσεις που ελέγχουν αν το πεδίο είναι ίσο ή διάφορο του null και οι οποίες πληρούν ένα σύνολο κριτηρίων που καθιστούν 83
δυνατή τη διαγραφή τους. Μόνο τα πεδία για τα οποία θα ανιχνευτούν υποθετικές δηλώσεις υποψήφιες για διαγραφή θα θεωρηθούν τελικά κατάλληλα για την εφαρμογή της αναδόμησης προς το πρότυπο Null Object, μιας και μόνο τότε θα έχουμε όφελος από τη χρήση του προτύπου. Τα βήματα του αλγορίθμου εντοπισμού αυτών των υποθετικών δηλώσεων είναι τα ακόλουθα: 1. Για κάθε υποψήφιο μέχρι στιγμής πεδίο εντοπίζουμε στην Context κλάση στην οποία ορίζεται, δομές if else το πολύ δυο διακλαδώσεων (branches) που έχουν έκφραση ελέγχου (expression) αν το πεδίο είναι ίσο ή διάφορο του null. Οι προϋποθέσεις προκειμένου μια τέτοια δομή if να θεωρηθεί υποψήφια για διαγραφή είναι οι ακόλουθες (εξετάζεται η περίπτωση που η έκφραση ελέγχει αν το πεδίο είναι διάφορο του null): a. Αν η δομή if έχει μία διακλάδωση, στο then σώμα της θα πρέπει να καλούνται μόνο μέθοδοι του πεδίου. Οι μέθοδοι αυτές δε θα πρέπει να είναι στατικής εμβέλειας (static), δε θα πρέπει να έχουν χαρακτηριστεί από τη μέχρι τώρα ανάλυση ως methodsthrowingapplicationspecificexception (βλέπε προϋπόθεση 1.c. παρακάτω) και δε θα πρέπει να έχουν χαρακτηριστεί ως μέθοδοι που ο τρόπος κλήσης τους μέσα στην κλάση μπορεί να οδηγήσει στη «ρίψη» Null Pointer Exception (methodscausingnpe). Μια μέθοδος ανήκει στην κατηγορία των methodscausingnpe αν δεν ανήκει σε μια από τις παρακάτω περιπτώσεις: i. Καλείται εντός του then σώματος κάποιας δομής if με έκφραση ελέγχου αν το πεδίο είναι διάφορο του null και όλοι οι τελεστές της έκφρασης (αν υπάρχουν) είναι «και (and)» (&&) ή ii. Καλείται εντός του else σώματος κάποιας δομής if (δομής if else... ή δομής if... else if... else...) όπου τουλάχιστον μία από τις προηγούμενες εκφράσεις περιέχει έλεγχο αν το πεδίο είναι ίσο με null και όλοι οι τελεστές της συγκεκριμένης έκφρασης (αν υπάρχουν) είναι «ή (or)» ( ). b. Αν στο then σώμα της δομής if που διαθέτει μία μόνο διακλάδωση, καλείται μια non-void μέθοδος του πεδίου, τότε η δομή if θα χαρακτηριστεί ως υποψήφια για διαγραφή μόνο αν επιπλέον ισχύει ότι ο επιστρεφόμενος τύπος 84
(return type) της non-void μεθόδου είναι πρωταρχικός τύπος (primitive type) ή String και η τιμή της μεταβλητής στην οποία τίθεται η επιστρεφόμενη τιμή της μεθόδου είναι γνωστή σε χρόνο μεταγλώττισης (compilation time). Οι void μέθοδοι του πεδίου που καλούνται στο then σώμα της δομής if, χαρακτηρίζονται ως μέθοδοι που θα έχουν κενό σώμα (methodshavingemptybody) στην κλάση Null object που θα δημιουργηθεί κατά την εφαρμογή της αναδόμησης, ενώ οι non-void μέθοδοι χαρακτηρίζονται ως μέθοδοι που θα επιστρέφουν κάποια τιμή (methodsreturningavalue). c. Αν η δομή if έχει δύο διακλαδώσεις, τότε στο else σώμα της δομής if θα πρέπει να υπάρχει μόνο μια throw εντολή που θα δίδει ένα νέο στιγμιότυπο μιας Exception κλάσης του project (application specific exception). Τα ορίσματα (arguments) του κατασκευαστή της application specific exception κλάσης θα πρέπει να είναι πρωταρχικού τύπου ή String και οι τιμές τους θα πρέπει να είναι γνωστές σε χρόνο μεταγλώττισης. Επίσης, η προϋπόθεση 1.a. αλλάζει και αρκεί μόνο η πρώτη μέθοδος που καλείται στο then σώμα της δομής if να είναι μέθοδος του πεδίου, που δεν είναι static, ούτε methodcausingnpe. Επίσης, δε θα πρέπει να έχει χαρακτηριστεί από τη μέχρι τώρα ανάλυση ως methodhavingemptybody ή ως methodreturningavalue αν είναι non-void μέθοδος. Η προϋπόθεση 1.b. δεν είναι αναγκαίο να ισχύει. Η πρώτη μέθοδος του πεδίου που καλείται στο then σώμα της δομής if, θα χαρακτηριστεί ως μέθοδος που θα δίδει κάποια application specific exception στην κλάση Null object που θα δημιουργηθεί κατά την εφαρμογή της αναδόμησης (methodthrowingapplicationspecificexception). 2. Σε περίπτωση που η έκφραση της δομής if ελέγχει αν το πεδίο είναι ίσο με null, a. η δομή if των δύο διακλαδώσεων θα θεωρηθεί υποψήφια για διαγραφή αν ισχύει η προϋπόθεση 1.c. όπως περιγράφεται, αλλά κατοπτρικά, δηλαδή για το then σώμα και το else σώμα της δομής αντίστοιχα. b. Αν η δομή if με έκφραση ελέγχου αν το πεδίο είναι ίσο με null είναι μίας διακλάδωσης, θα θεωρείται υποψήφια για διαγραφή αν στο then σώμα της υπάρχει μόνο μια throw εντολή που δίδει ένα νέο στιγμιότυπο μιας Exception κλάσης του project (application specific exception). Τα ορίσματα του 85
κατασκευαστή της application specific exception κλάσης θα πρέπει να είναι πρωταρχικού τύπου ή String και οι τιμές τους θα πρέπει να είναι γνωστές σε χρόνο μεταγλώττισης. Σε αυτήν την περίπτωση στην κλάση Null Object θα δημιουργηθεί μια νέα μέθοδος με όνομα assertnotnull{i}, όπου {i}=0, 1, 2,, η οποία και θα δίδει τη συγκεκριμένη application specific exception, ενώ και στην κλάση Component θα προστεθεί η συγκεκριμένη μέθοδος που όμως θα είναι κενού σώματος. Στην Context κλάση, η δομή if θα αντικαθίσταται από την κλήση της συγκεκριμένης μεθόδου με καλούντα (invoker) το πεδίο component. Στην Εικόνα 21 που ακολουθεί παρουσιάζονται οι υποθετικές δηλώσεις για το πεδίο customer της κλάσης Room του παραδείγματος που παρουσιάστηκε στην ενότητα 4.2, καθώς και το πώς θα χαρακτηριστούν οι αντίστοιχες μέθοδοι της κλάσης Customer. 86
Εικόνα 21: Παραδείγματα υποθετικών δηλώσεων για το πεδίο customer και ο τρόπος που έχουν χαρακτηριστεί από τη μεθοδολογία 87
4.3.4 Ανάλυση των Προϋποθέσεων για τις Υποψήφιες για Διαγραφή Υποθετικές Δηλώσεις Στην ενότητα αυτή αναλύεται περαιτέρω το τμήμα του αλγορίθμου που αφορά την ανίχνευση, για κάθε υποψήφιο πεδίο, των υποθετικών δηλώσεων που μπορούν να χαρακτηριστούν υποψήφιες για διαγραφή, μιας και κρίνεται ότι χρήζει περαιτέρω αποσαφήνισης. Για κάθε πεδίο που έχει χαρακτηριστεί από τη μέχρι τώρα διαδικασία ανίχνευσης ως υποψήφιο για την εφαρμογή του προτύπου, εντοπίζονται μέσα στην Context κλάση όλες οι υποθετικές δομές if, μίας ή δύο το πολύ διακλαδώσεων, η έκφραση ελέγχου των οποίων εξετάζει αν το πεδίο ισούται ή όχι με null. Πρέπει να αναφερθεί ότι εντοπίζονται όλων των μορφών οι εκφράσεις που ελέγχουν αν το πεδίο είναι ίσο ή διάφορο του null, δηλαδή οι ακόλουθες μορφές εκφράσεων ελέγχου: component!=null ή component==null null!=component ή null==component this.component!=null ή this.component==null null!=this.component ή null==this.component Οι παραπάνω υποθετικές δομές θα εξεταστούν στη συνέχεια κατά πόσο πληρούν ένα σύνολο προϋποθέσεων που έχουν αναπτυχθεί στα πλαίσια της μεθοδολογίας, ώστε να είναι δυνατή η διαγραφή τους και συνεπώς η εφαρμογή του προτύπου Null Object. Οι προϋποθέσεις αυτές αναπτύχθηκαν με κύριο σκοπό κατά την εφαρμογή της αναδόμησης να διασφαλίζεται η διατήρηση της συμπεριφοράς του υπό εξέταση κώδικα. Μόνο τα πεδία για τα οποία θα εντοπιστούν δομές if υποψήφιες για διαγραφή θα προταθούν τελικά ως κατάλληλα για την εφαρμογή του προτύπου. Για αυτά τα κατάλληλα πεδία, για τα οποία συγχρόνως, όμως, έχει ανιχνευτεί σε προηγούμενο στάδιο ότι επιτρέπεται η αυτοματοποιημένη εκτέλεση της αναδόμησης, θα δίνεται στο χρήστη η δυνατότητα επιλογής τους ώστε το εργαλείο να εκτελέσει αυτοματοποιημένα την εφαρμογή της δομής του προτύπου. Προκειμένου μια δομή if να θεωρηθεί υποψήφια για διαγραφή θα πρέπει να εξεταστεί, επιπλέον, τόσο το then σώμα της όσο και το else σώμα της σε περίπτωση που υπάρχει. Οι δομές if που εντοπίζονται μέσα στην Context κλάση εξετάζονται με τη σειρά που εμφανίζονται μέσα στην κλάση. Αναλόγως του είδους των δομών if που 88
θα χαρακτηριστούν ως υποψήφιες για διαγραφή και της μορφής τόσο του then όσο και του else σώματός τους, θα προκύψει και το τι σώμα θα έχουν οι μέθοδοι κατά τη δήλωσή τους στην κλάση NullComponent κατά την εφαρμογή της αναδόμησης. Όλες αυτές οι επιλογές γίνονται πάντα με γνώμονα την εξασφάλιση της ίδιας συμπεριφοράς του κώδικα πριν και μετά την εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Προς την κατεύθυνση της διατήρησης της συμπεριφοράς του κώδικα, στοχεύει και η διαδικασία του εντοπισμού των μεθόδων της κλάσης Component, που ο τρόπος κλήσης τους μέσω του πεδίου component στην κλάση Context μπορεί να οδηγήσει στην πρόκληση Null Pointer Exception (methodscausingnpe). Αυτές οι μέθοδοι θα πρέπει να υλοποιηθούν στην κλάση NullComponent ώστε να εγείρουν και πάλι Null Pointer Exception. Για το λόγο αυτό οι μέθοδοι αυτές εντοπίζονται προτού ξεκινήσει η διαδικασία εύρεσης των υποψήφιων για διαγραφή υποθετικών δομών (Βλ. βήματα 1.a.i. και 1.a.ii. του αλγορίθμου εντοπισμού υποθετικών δηλώσεων υποψήφιων για διαγραφή που παρουσιάστηκε στην ενότητα 4.3.3), ώστε να γνωρίζουμε ότι αυτές μπορούν μόνο να υλοποιηθούν εγείροντας Null Pointer Exception κατά την εφαρμογή του προτύπου Null Object. 4.3.4.1 Ανάλυση των Διαφορετικών Ειδών Υποθετικών Δομών Στην ενότητα αυτή θα αναλυθούν οι προϋποθέσεις που αφορούν στο σώμα των υποθετικών δηλώσεων, προκειμένου αυτές να θεωρηθούν υποψήφιες για διαγραφή. 1. Ανάλυση των προϋποθέσεων που αφορούν στις δομές if μίας διακλάδωσης με έκφραση ελέγχου αν το πεδίο είναι διάφορο του null (component!=null). Προκειμένου, λοιπόν, να είναι επιτρεπτή και θεμιτή η διαγραφή μιας τέτοιας δομής και η αντικατάστασή της με τις εντολές του then σώματός της, με ταυτόχρονη διατήρηση της συμπεριφοράς του κώδικα, το then σώμα της θα πρέπει να περιέχει μόνο κλήσεις μεθόδων της κλάσης του τύπου του πεδίου, Component, με καλούντα (invoker) το ίδιο το πεδίο (Βλ. βήμα 1.a. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Επιπλέον, οι μέθοδοι αυτές δεν πρέπει να είναι στατικής εμβέλειας (static) (Βλ. βήμα 1.a. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Η προϋπόθεση αυτή υιοθετήθηκε διότι σε διαφορετική περίπτωση αυτές οι στατικές μέθοδοι θα έπρεπε να 89
μετακινηθούν μαζί με το σώμα τους όσο και με τα τυχόν static πεδία που χρησιμοποιούν στην κλάση AbstractComponent. Αυτή η μετακίνηση θα ήταν επιβεβλημένη μιας και αφενός το πεδίο component μετά την αναδόμηση θα δηλωθεί ως τύπου AbstractComponent και αφετέρου οι μέθοδοι στατικής εμβέλειας δε θα μπορούσαν να δηλωθούν ως αφηρημένες (abstract) στην κλάση AbstractComponent. Αυτή η μεταφορά, όμως, θα δημιουργούσε προβλήματα καθότι δε θα ήταν δυνατή η διατήρηση της συμπεριφοράς του κώδικα στην περίπτωση της συγκεκριμένης δομής if όταν το πεδίο component θα ήταν στιγμιότυπο (instance) της κλάσης NullComponent. Αυτό πηγάζει από το γεγονός ότι το ποια στατική μέθοδος θα κληθεί εξαρτάται μόνο από το δηλωμένο (declared) τύπο και όχι από τον υλοποιημένο (instantiated). Εφόσον πληρούνται οι παραπάνω προϋποθέσεις, συμπεραίνουμε ότι η συγκεκριμένη δομή if μπορεί να διαγραφεί και οι μέθοδοι που καλούνται στο then σώμα της θα δηλωθούν στην κλάση NullComponent να έχουν κενό σώμα (methodhavingemptybody), σε περίπτωση φυσικά που είναι μέθοδοι χωρίς επιστρεφόμενο τύπο (void). Για να μην υπάρχει, όμως, σύγκρουση του τρόπου δήλωσης των μεθόδων στην κλάση NullComponent και για να μην παραβιαστεί η αρχή της διατήρησης της συμπεριφοράς του κώδικα, μια επιπλέον προϋπόθεση που θα πρέπει να ισχύει είναι ότι οι συγκεκριμένες μέθοδοι από τη μέχρι στιγμής εξέταση των προηγούμενων υποθετικών δομών if, δε θα πρέπει να έχουν χαρακτηριστεί ότι θα έχουν διαφορετική συμπεριφορά κατά την κλήση τους μέσω instance της κλάσης NullComponent. Αυτό σημαίνει ότι δε θα πρέπει να έχουν χαρακτηριστεί ούτε ως methodscausingnpe ούτε ως methodsthrowingapplicationspecificexception (Βλ. βήμα 1.a. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Αναφορικά με την περίπτωση που στο then σώμα της υποψήφιας για διαγραφή υποθετικής δομής καλείται μια non-void μέθοδος του πεδίου component, η επιστρεφόμενη τιμή της οποίας ανατίθεται σε κάποια τοπική μεταβλητή, προκειμένου μια τέτοια δομή να θεωρηθεί υποψήφια για διαγραφή, θα πρέπει να εξασφαλιστεί η διατήρηση της συμπεριφοράς του κώδικα σε περίπτωση που το πεδίο είναι instance της κλάσης NullComponent (Βλ. βήμα 1.b. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Σε αυτήν την περίπτωση επιβάλλεται η τιμή που θα επιστρέφει η εν λόγω μέθοδος στην κλάση NullComponent να ισούται με αυτήν που είχε η τοπική 90
μεταβλητή ακριβώς πριν την εκτέλεση της υποθετικής δήλωσης. Για το λόγο αυτό είναι απαραίτητο να εντοπιστεί η τελευταία, πριν την εκτέλεση της υποθετικής λογικής, δήλωση ή ανάθεση στην τοπική μεταβλητή. Για να εξασφαλιστεί αυτή η πληροφορία, καθίσταται αναγκαία η χρήση τόσο στατικών πληροφοριών, σχετικά με τις εντολές του πηγαίου κώδικα, όσο και πληροφοριών που σχετίζονται με τη ροή του ελέγχου και των δεδομένων στο πρόγραμμα. Οι αναπαραστάσεις που παρέχουν αυτού του είδους τις πληροφορίες είναι ο γράφος ροής ελέγχου (Control Flow Graph - CFG) και ο γράφος εξαρτήσεων προγράμματος (Program Dependence Graph - PDG), ο οποίος περιλαμβάνει τόσο τις εξαρτήσεις ελέγχου όσο και τις εξαρτήσεις δεδομένων. Οι γράφοι αυτοί δημιουργούνται για τη μέθοδο στο σώμα της οποίας βρίσκεται η υποψήφια για διαγραφή υποθετική δήλωση που εξετάζεται κάθε φορά. Η δημιουργία των γράφων βασίστηκε στη σχετική υλοποίηση που είχε αναπτυχθεί στο JDeodorant στα πλαίσια της ανίχνευσης περιπτώσεων αναδόμησης με εξαγωγή μεθόδου, όπως περιγράφεται στο [46]. Ουσιαστικά ο γράφος CFG της μεθόδου έχει βοηθητική λειτουργία και χρησιμοποιείται ως ενδιάμεσο στάδιο για την κατασκευή του αντίστοιχου γράφου PDG της μεθόδου. Ακολούθως, ο γράφος PDG αξιοποιείται για την εύρεση του συνόλου των boundary blocks της εντολής ανάθεσης η οποία και εξετάζεται, δηλαδή της εντολής, εντός του then σώματος της υποθετικής δήλωσης, η οποία έχει δεξί μέλος την κλήση της μεθόδου με καλούντα το πεδίο component. Στη συνέχεια υπολογίζεται το slice της εντολής ανάθεσης, με σκοπό να εντοπιστεί η τελευταία, πριν την εκτέλεση της υποθετικής λογικής, δήλωση ή ανάθεση στην τοπική μεταβλητή. Εφόσον αυτή η τελευταία δήλωση ή ανάθεση βρίσκεται σε όλα τα πιθανά μονοπάτια ελέγχου που καταλήγουν στην υποθετική δήλωση, εξετάζεται η τιμή της. Η προϋπόθεση αυτή είναι απαραίτητη ώστε να εξασφαλιστεί ότι η τιμή της μεταβλητής είναι η συγκεκριμένη, ανεξαρτήτως της ροής ελέγχου που μπορεί να ακολουθηθεί στο πρόγραμμα. Ο έλεγχος της συγκεκριμένης προϋπόθεσης βασίστηκε στη διάσχιση του γράφου CFG. Πιο συγκεκριμένα, με βάση την πληροφορία που αντλείται από τους γράφους, η μεθοδολογία ανίχνευσης που αναπτύχθηκε περιλαμβάνει τα εξής βήματα: 1. Εύρεση της εντολής ανάθεσης η οποία και εξετάζεται. 91
2. Εύρεση της τοπικής μεταβλητής που είναι στο αριστερό μέλος της εντολής ανάθεσης. 3. Κατασκευή του γράφου ροής ελέγχου (CFG) της μεθόδου με την υποθετική λογική. 4. Κατασκευή του γράφου εξαρτήσεων του προγράμματος (PDG) βάσει του CFG που δημιουργήθηκε στο προηγούμενο βήμα για τη μέθοδο με την υποθετική λογική. 5. Εύρεση του κόμβου υποθετικής λογικής και του κόμβου που αντιστοιχεί στην εντολή ανάθεσης στο PDG διάγραμμα. 6. Εύρεση όλων των κόμβων του PDG διαγράμματος με τις εντολές ανάθεσης στην τοπική μεταβλητή καθώς και του κόμβου με τη δήλωση της τοπικής μεταβλητής. 7. Υπολογισμός του συνόλου των boundary blocks κάθε κόμβου του προηγούμενου βήματος 6. 8. Υπολογισμός της τομής των συνόλων των boundary blocks που υπολογίστηκαν στο βήμα 7. 9. Υπολογισμός του slice της εντολής ανάθεσης που εξετάζεται. 10. Εντοπισμός στο slice της τελευταίας εντολής ανάθεσης ή εντολής δήλωσης που βρίσκεται πριν την υπό εξέταση υποθετική δήλωση. 11. Εξασφάλιση ότι ο κόμβος της τελευταίας εντολής ανάθεσης ή δήλωσης που βρέθηκε στο βήμα 10 βρίσκεται σε όλα τα μονοπάτια ελέγχου που ξεκινούν από την αρχή της μεθόδου και καταλήγουν στην υπό εξέταση εντολή ανάθεσης. Αφού εντοπιστεί, λοιπόν, το τελευταίο σημείο του κώδικα, πριν την υποθετική δομή, όπου παίρνει τιμή η τοπική μεταβλητή, είναι, επιπλέον, απαραίτητο να βρεθεί η τιμή της τοπικής μεταβλητής σε αυτό το σημείο. Αυτό υπαγορεύει την προσθήκη μιας νέας προϋπόθεσης για να είναι δυνατή η διαγραφή της υποθετικής δήλωσης, δηλαδή τη γνώση της τιμής της τοπικής μεταβλητής σε χρόνο μεταγλώττισης (compilation time) και όχι σε χρόνο εκτέλεσης (runtime). Η πλήρης επίλυση του προβλήματος αυτού απαιτεί να ληφθεί μέριμνα τόσο για την περίπτωση όπου η τοπική μεταβλητή έχει πρωταρχικό τύπο ή String, καθώς και για την περίπτωση που είναι αντικείμενο (object). Στην περίπτωση του αντικειμένου, η πολυπλοκότητα του ελέγχου και της επαναδημιουργίας του τελευταίου instance του αυξάνεται εκθετικά, καθώς απαραίτητη προϋπόθεση θα ήταν ο προσδιορισμός των 92
τιμών όλων των πεδίων αυτού και σε περίπτωση που τα πεδία αυτά ήταν πάλι αντικείμενα, ο έλεγχος θα έπρεπε να εκτελείται αναδρομικά για καθένα από αυτά μέχρις ότου αποκλεισθεί το ενδεχόμενο η τιμή τους να είναι γνωστή σε χρόνο μεταγλώττισης. Κατά συνέπεια, στα πλαίσια της παρούσας εργασίας απαραίτητη προϋπόθεση προκειμένου μια υποθετική δομή να χαρακτηριστεί υποψήφια για διαγραφή είναι ο επιστρεφόμενος τύπος (return type) των non-void μεθόδων που καλούνται στο then σώμα της να είναι πρωταρχικού τύπου ή String. Επιπλέον, θα πρέπει η τιμή της τοπικής μεταβλητής στο σημείο του κώδικα πριν την υποθετική δήλωση, να είναι γνωστή σε χρόνο μεταγλώττισης. Εφόσον πληρούνται οι παραπάνω προϋποθέσεις, η εν λόγω μέθοδος της κλάσης NullComponent θα οριστεί σαν methodreturningavalue και θα δηλωθεί ώστε να επιστρέφει την τιμή που υπολογίστηκε με τη μεθοδολογία που περιγράφηκε προηγουμένως. Πρέπει να διευκρινιστεί ότι η επιστρεφόμενη τιμή της κλήσης της non-void μεθόδου του πεδίου component πρέπει να τίθεται σε κάποια τοπική μεταβλητή και όχι σε κάποιο πεδίο της Context κλάσης, ώστε η εν λόγω υποθετική δήλωση να είναι υποψήφια για διαγραφή. Αυτή η προϋπόθεση κρίθηκε αναγκαία διότι η εύρεση της τιμής του πεδίου ακριβώς πριν την υποθετική δήλωση ενδέχεται να είναι αδύνατη. Αν δεν υπάρχει ανάθεση στην τιμή του πεδίου στα πλαίσια της μεθόδου που ορίζεται η υποθετική δήλωση, δεν είναι πρακτικά εφικτό να εξεταστούν όλοι οι πιθανοί τρόποι που έχει πάρει τιμή το πεδίο. Κάτι τέτοιο θα απαιτούσε εκτεταμένους ελέγχους που ενδέχεται να εκτείνονταν και σε όλη την Context κλάση, αυξάνοντας κατά πολύ τις απαιτήσεις σε μνήμη και το χρόνο εκτέλεσης της διαδικασίας ανίχνευσης. 2. Ανάλυση των προϋποθέσεων που αφορούν στις δομές if δύο διακλαδώσεων με έκφραση ελέγχου αν το πεδίο είναι διάφορο του null (component!=null). Η δομή if δύο διακλαδώσεων θα θεωρηθεί υποψήφια για διαγραφή μόνο αν στο else σώμα της υπάρχει μόνο μια throw εντολή που δίδει ένα νέο στιγμιότυπο μιας Exception κλάσης του project (Βλ. βήμα 1.c. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Αναφορικά με το then σώμα της, σε μια τέτοια περίπτωση αρκεί μόνο η πρώτη μέθοδος που καλείται να είναι μέθοδος του πεδίου, που δεν είναι static, ούτε methodcausingnpe και δεν έχει χαρακτηριστεί από τη μέχρι τώρα ανάλυση ως methodhavingemptybody ή ως methodreturningavalue αν είναι nonvoid μέθοδος. Προκειμένου να εξασφαλιστεί η αρχή της διατήρησης της 93
συμπεριφοράς μετά την αναδόμηση, η πρώτη μέθοδος του then σώματος της υποθετικής δήλωσης θα ορισθεί στην κλάση NullComponent ώστε να δίδει την application specific exception που εντοπίστηκε στο else σώμα και θα χαρακτηριστεί ως methodthrowingapplicationspecificexception. Προφανώς αυτό απαιτεί τα ορίσματα του κατασκευαστή της application specific κλάσης που εγείρεται να είναι γνωστά σε χρόνο μεταγλώττισης και συνεπώς, για λόγους μείωσης της πολυπλοκότητας του ελέγχου, να είναι μόνο πρωταρχικού τύπου ή String. 3. Ανάλυση των προϋποθέσεων που αφορούν στις δομές if δύο διακλαδώσεων με έκφραση ελέγχου αν το πεδίο είναι ίσο με null (component==null). Αυτή η περίπτωση υποθετικής δήλωσης είναι κατοπτρική της περίπτωσης 2, πράγμα που σημαίνει ότι οι προϋποθέσεις της περίπτωσης 2 που αφορούν στο then σώμα της τώρα θα αφορούν στο else σώμα της και τανάπαλιν (Βλ. βήμα 2.a. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). 4. Ανάλυση των προϋποθέσεων που αφορούν στις δομές if μίας διακλάδωσης με έκφραση ελέγχου αν το πεδίο είναι ίσο με null (component==null). Σημαντική είναι η μέριμνα της προτεινόμενης μεθοδολογίας να μειώσει τις υποθετικές δηλώσεις αυτής της μορφής, οι οποίες ουσιαστικά δεν υπάρχουν για να προστατέψουν από NPE, αλλά για να προκαλέσουν μια συγκεκριμένη συμπεριφορά όταν το πεδίο είναι null. Αυτές οι δομές, λοιπόν, θα θεωρηθούν υποψήφιες για διαγραφή μόνο αν στο then σώμα τους υπάρχει μόνο μία throw εντολή που δίδει ένα νέο στιγμιότυπο μιας application specific exception κλάσης (Βλ. βήμα 2.b. του αλγορίθμου εντοπισμού της ενότητας 4.3.3). Για τους ίδιους λόγους που αναλύθηκαν στην περίπτωση της 2 ης μορφής υποθετικής δήλωσης, τα ορίσματα του κατασκευαστή της application specific exception κλάσης θα πρέπει να είναι πρωταρχικού τύπου ή String και οι τιμές τους θα πρέπει να είναι γνωστές σε χρόνο μεταγλώττισης. 4.3.5 Εντοπισμός των Suggested Refactorable Πεδίων Η προτεινόμενη μεθοδολογία δύναται να εντοπίσει και τα πεδία τα οποία είναι μεν προτεινόμενα για την εφαρμογή του προτύπου Null Object, αλλά η εκτέλεση της αναδόμησης προς αυτό το πρότυπο δεν είναι δυνατό να αυτοματοποιηθεί και απαιτεί την παρέμβαση του προγραμματιστή, ώστε να κάνει τις απαραίτητες τροποποιήσεις στον κώδικα. Ως suggested refactorable πεδία εντοπίζονται, λοιπόν, τα πεδία τα οποία 94
πληρούν τις προϋποθέσεις τόσο της ενότητας 4.3.1 όσο και της ενότητας 4.3.3, αλλά δεν είναι δυνατή η αυτοματοποίηση της εφαρμογής της αναδόμησης διότι ανήκουν σε κάποια από τις περιπτώσεις της ενότητας 4.3.2. 4.4 Εφαρμογή Αναδόμησης Κατά την αναδόμηση με χρήση του προτύπου Null Object, οι μετασχηματισμοί που εφαρμόζονται στο AST δέντρο αποσκοπούν στην υλοποίηση των δομικών στοιχείων του προτύπου στο αναδομούμενο σύστημα, καθώς και στην εξασφάλιση της διατήρησης της αρχικής συμπεριφοράς του υπό εξέταση συστήματος. Τα δομικά στοιχεία από τα οποία αποτελείται το πρότυπο Null Object όταν ακολουθείται η προσέγγιση δημιουργίας αφηρημένης υπερκλάσης, όπως παρουσιάστηκαν διεξοδικά στην ενότητα 3.1, είναι η κλάση πελάτης Context που δηλώνει το υποψήφιο πεδίο, το πεδίο για το οποίο διαγράφονται οι έλεγχοι, η αφηρημένη κλάση και οι συγκεκριμένες υποκλάσεις της. Κατά συνέπεια, οι εκτελούμενοι μετασχηματισμοί της αναδόμησης, συνοψίζονται στη δημιουργία της αφηρημένης κλάσης και της συγκεκριμένης Null Object υποκλάσης της, οι μέθοδοι της οποίας θα έχουν την κατάλληλη συμπεριφορά ώστε να είναι δυνατή η διαγραφή όσο το δυνατόν περισσότερων υποθετικών δηλώσεων από την Context κλάση. Επιπλέον, το επιλεγμένο πεδίο θα πρέπει να δηλωθεί με τύπο την αφηρημένη υπερκλάση και να αρχικοποιηθεί με ένα στιγμιότυπο της Null Object υποκλάσης. Τέλος, η κλάση πελάτης, Context, πρέπει να τροποποιηθεί καταλλήλως, ώστε να διαγραφούν οι υποθετικές δηλώσεις που δύναται να διαγραφούν, αλλά και να διατηρηθεί ίδια η συμπεριφορά του λογισμικού. Στη συνέχεια περιγράφεται αναλυτικά η διαδικασία της εφαρμογής της αναδόμησης. 1. Ο χρήστης επιλέγει το πεδίο με όνομα component, που είναι έστω τύπου Component, για το οποίο θέλει αυτόματα να εφαρμοστεί το πρότυπο Null Object. 2. Μια ιεραρχία κλάσεων δημιουργείται, η οποία αποτελείται από την abstract κλάση με όνομα της μορφής Abstract{Το_Όνομα_του_Πεδίου}, δηλαδή στην προκειμένη περίπτωση AbstractComponent, η οποία ορίζεται ως υπερκλάση της κλάσης Component και από την κλάση με όνομα της μορφής Null{Το_Όνομα_του_Πεδίου}, δηλαδή NullComponent, η οποία ορίζεται ως υποκλάση της κλάσης AbstractComponent (Βλ. και Εικόνα 22 για το παράδειγμα Room Customer). Αν η κλάση Component ορίζεται μόνη της σε ένα Java 95
αρχείο, τότε τόσο η κλάση AbstractComponent όσο και η NullComponent θα οριστούν σε ξεχωριστά αρχεία η καθεμία, στο ίδιο πακέτο (package) με την κλάση Component. Αν η κλάση Component ορίζεται μέσα σε ένα αρχείο μαζί με κάποια άλλη κλάση, σε αυτό το αρχείο θα οριστούν και οι κλάσεις AbstractComponent και NullComponent. Ενώ, αν η κλάση Component ορίζεται ως inner κλάση μέσα στο σώμα μιας άλλης κλάσης, με αυτόν τον τρόπο θα οριστούν και οι κλάσεις AbstractComponent και NullComponent. Εικόνα 22: Η αλλαγή στη δομή του κώδικα μετά την εφαρμογή της αναδόμησης για το πεδίο customer 3. Η κλάση AbstractComponent θα οριστεί ως ακολούθως (Βλ. και Εικόνα 23 για την κλάση AbstractCustomer): a. Θα έχει τους ίδιους modifiers (εκτός από final) με την κλάση Component και θα δηλωθεί να υλοποιεί όλα τα interfaces που τυχόν υλοποιούσε η κλάση Component. b. Αν η κλάση Component ορίζεται μόνη της μέσα σε ένα αρχείο ή ορίζεται μέσα σε ένα αρχείο μαζί με κάποια άλλη κλάση, τότε κάθε μη private και μη static μέθοδος της κλάσης Component θα οριστεί στην AbstractComponent κλάση ως abstract, διατηρώντας τους ίδιους modifiers (εκτός από τους synchronized, final και strictfp), τον ίδιο επιστρεφόμενο τύπο, το ίδιο όνομα και τις ίδιες παραμέτρους και δε θα έχει σώμα. c. Αν η κλάση Component ορίζεται ως inner κλάση μέσα στο σώμα μιας άλλης κλάσης (outer κλάση), τότε κάθε μη static μέθοδος της κλάσης 96
Component θα οριστεί στην AbstractComponent κλάση ως abstract, διατηρώντας τους ίδιους modifiers (εκτός από τους synchronized, final και strictfp), τον ίδιο επιστρεφόμενο τύπο, το ίδιο όνομα και τις ίδιες παραμέτρους και δε θα έχει σώμα. Στη συγκεκριμένη περίπτωση και οι private μέθοδοι θα οριστούν στην αφηρημένη υπερκλάση, προκειμένου να εξασφαλιστεί η διατήρηση της συμπεριφοράς στην outer κλάση σε περίπτωση που αντιστοιχεί στην Context κλάση και καλεί κάποια από αυτές τις μεθόδους μέσω του πεδίου component, μιας και η outer κλάση έχει πρόσβαση και στις μεθόδους ιδιωτικής ορατότητας της inner κλάσης. d. Σε περίπτωση που κατά τη φάση της ανίχνευσης η μέθοδος είχε χαρακτηριστεί ως methodthrowingapplicationspecificexception, τότε στην AbstractComponent κλάση η αντίστοιχη μέθοδος θα δηλωθεί να δίδει τη συγκεκριμένη exception που είχε ανιχνευθεί. e. Σε περίπτωση που κατά τη φάση της ανίχνευσης είχαν εντοπιστεί περιπτώσεις που απαιτείται η δημιουργία νέων μεθόδων assertnotnull{i}, με {i}=0, 1, 2,, (Βλ. περίπτωση 2.b. του αλγορίθμου εντοπισμού της ενότητας 4.3.3), τότε αυτές οι μέθοδοι θα οριστούν στην κλάση AbstractComponent ως abstract void με δημόσια ορατότητα και θα δηλωθούν ότι θα δίδουν την συγκεκριμένη application specific exception που είχε εντοπιστεί στη φάση της ανίχνευσης. f. Στην κλάση AbstractComponent θα προστεθούν, τέλος, οι ακόλουθες μέθοδοι: public abstract boolean isnull(); public abstract Component getreference(); Η πρώτη μέθοδος κρίθηκε απαραίτητη ώστε να διατηρηθεί ίδια η συμπεριφορά του κώδικα στην περίπτωση των υποθετικών δηλώσεων η έκφραση ελέγχου των οποίων ελέγχει αν το πεδίο ισούται ή όχι με null, οι οποίες, όμως, δεν πληρούν τις προϋποθέσεις ώστε να απομακρυνθούν από τον κώδικα (Βλ. βήμα 6.d. του αλγορίθμου παρακάτω). Ενώ η δεύτερη είναι αναγκαία ώστε να εξασφαλιστεί ότι το πεδίο δε θα «περνά» ως στιγμιότυπο της κλάσης NullComponent κατά την αλληλεπίδρασή του εκτός της Context κλάσης. 97
Εικόνα 23: Η δημιουργηθείσα κλάση AbstractCustomer 4. Η κλάση NullComponent θα οριστεί ως ακολούθως (Βλ. και Εικόνα 24 για την κλάση NullCustomer): a. Θα έχει τους ίδιους modifiers (εκτός από abstract) με την κλάση Component και θα δηλωθεί ως υποκλάση της κλάσης AbstractComponent, αλλά δε θα δηλωθεί να υλοποιεί τα interfaces που τυχόν υλοποιούσε η κλάση Component. b. Αν η κλάση Component ορίζεται μόνη της μέσα σε ένα αρχείο ή ορίζεται μέσα σε ένα αρχείο μαζί με κάποια άλλη κλάση, τότε κάθε μη private και μη static μέθοδος της κλάσης Component θα οριστεί στην κλάση NullComponent διατηρώντας τους ίδιους modifiers (εκτός από abstract), τον ίδιο επιστρεφόμενο τύπο, το ίδιο όνομα και τις ίδιες παραμέτρους. Αντιθέτως, αν η κλάση Component ορίζεται ως inner κλάση μέσα στο σώμα μιας άλλης κλάσης, τότε κάθε μη static μέθοδος της κλάσης Component θα οριστεί στην NullComponent κλάση διατηρώντας τους ίδιους modifiers (εκτός από abstract), τον ίδιο επιστρεφόμενο τύπο, το ίδιο όνομα και τις ίδιες παραμέτρους. 98
c. Αναλόγως του πως είχε χαρακτηριστεί η μέθοδος κατά τη φάση της ανίχνευσης, δηλαδή ως methodhavingemptybody ή methodreturningavalue ή methodthrowingapplicationspecificexception ή methodcausingnpe, κατ αντιστοιχία η μέθοδος στην κλάση NullComponent, είτε θα έχει κενό σώμα, είτε θα επιστρέφει μια συγκεκριμένη τιμή, είτε θα δίδει μια application specific exception, είτε θα δίδει Null Pointer Exception. Αν η μέθοδος δεν είχε χαρακτηριστεί με κάποιον από τους παραπάνω τρόπους τότε θα οριστεί να δίδει UnsupportedOperationException. d. Σε περίπτωση που κατά τη φάση της ανίχνευσης είχαν εντοπιστεί περιπτώσεις που απαιτείται η δημιουργία νέων μεθόδων assertnotnull{i}, με {i}=0, 1, 2,, (Βλ. περίπτωση 2.b. αλγορίθμου εντοπισμού της ενότητας 4.3.3), τότε αυτές οι μέθοδοι θα οριστούν στην κλάση NullComponent ως void με δημόσια ορατότητα και στο σώμα τους θα δίδουν τη συγκεκριμένη application specific exception που είχε εντοπιστεί στη φάση της ανίχνευσης. e. Στην κλάση NullComponent θα προστεθούν, τέλος, οι ακόλουθες μέθοδοι: public boolean isnull(){ return true; } public Component getreference(){ return null; } 99
Εικόνα 24: Η δημιουργηθείσα κλάση NullCustomer 5. Η κλάση Component θα τροποποιηθεί ώστε (Βλ. και Εικόνα 25 για την κλάση Customer): a. Να οριστεί ως υποκλάση της κλάσης AbstractComponent, ενώ θα διαγραφούν από τη δήλωσή της τα interfaces που ενδεχομένως ορίζει ότι υλοποιεί. b. Θα προστεθούν οι ακόλουθες μέθοδοι: 100
public boolean isnull(){ return false; } public Component getreference(){ return this; } c. Σε περίπτωση που κατά τη φάση της ανίχνευσης είχαν εντοπιστεί περιπτώσεις που απαιτείται η δημιουργία νέων μεθόδων assertnotnull{i}, με {i}=0, 1, 2,, (Βλ. περίπτωση 2.b. αλγορίθμου εντοπισμού της ενότητας 4.3.3), τότε αυτές οι μέθοδοι θα οριστούν στην κλάση Component ως void με δημόσια ορατότητα και θα έχουν κενό σώμα. Εικόνα 25: Η μορφή της κλάσης Customer πριν και μετά την εφαρμογή της αναδόμησης 101
6. Η κλάση Context στην οποία δηλώνεται το πεδίο το οποίο είναι κατάλληλο για αυτόματη εφαρμογή του προτύπου, θα τροποποιηθεί ως ακολούθως (Βλ. και Εικόνα 26 για την κλάση Room): a. Θα αλλάξει η δήλωση του πεδίου, ώστε αυτό να οριστεί με τύπο AbstractComponent και να αρχικοποιηθεί με ένα αντικείμενο τύπου NullComponent. b. Θα προστεθεί η ακόλουθη μέθοδος: private AbstractComponent assigntocomponent (AbstractComponent c){ if (c==null){ return new NullComponent(); //Η αρχικοποίηση πρέπει //να προσαρμοστεί σε //περίπτωση που η κλάση //NullComponent είναι //inner κλάση κάποιας //άλλης κλάσης. }else{ return c; } } Η συγκεκριμένη μέθοδος θα έχει όνομα της μορφής assignto{το_όνομα_του_πεδίου} και είναι απαραίτητη ώστε να εξασφαλίσουμε ότι όλες οι αναθέσεις στο πεδίο component δε θα περνούν πλέον τιμή ίση με null στο πεδίο. c. Για κάθε δομή if που έχει χαρακτηριστεί κατά τη διαδικασία ανίχνευσης ως υποψήφια για διαγραφή, θα ισχύσουν τα εξής: i. Σε περίπτωση που είναι δομή με έκφραση ελέγχου αν ii. component!=null, τότε θα διαγραφεί η δομή και θα αντικατασταθεί από τις εντολές που βρίσκονται στο then σώμα της, ενώ σε περίπτωση που είναι δομή με έκφραση ελέγχου αν component==null, αν πρόκειται για δομή δύο διακλαδώσεων θα αντικατασταθεί από τις εντολές που βρίσκονται στο else σώμα της, ενώ αν πρόκειται για δομή μιας διακλάδωσης, από την κλήση της αντίστοιχης μεθόδου assertnotnull{i}, με {i}=0, 1, 2,, με καλούντα το πεδίο component. d. Για κάθε δομή if που δεν έχει χαρακτηριστεί κατά τη διαδικασία ανίχνευσης ως υποψήφια για διαγραφή και που περιλαμβάνει σε κάποιο σημείο της έκφρασης ελέγχου της την έκφραση component!=null ή component==null (καθώς και σε όλες τις άλλες πιθανές μορφές ελέγχου αν 102
το πεδίο είναι ίσο ή διάφορο του null), θα αντικαθίσταται η συγκεκριμένη έκφραση ως ακολούθως: i. Η έκφραση component!=null θα αντικαθίσταται από την έκφραση!component.isnull() ii. και η έκφραση component ==null θα αντικαθίσταται από την έκφραση component.isnull(). e. Όλες οι εντολές ανάθεσης που έχουν ως αριστερό μέλος το πεδίο (π.χ. component = x;), θα τροποποιηθούν ώστε το δεξί μέλος να αντικατασταθεί με την κλήση της μεθόδου assigntocomponent που θα έχει ως argument το παλιό δεξί μέλος (π.χ. component = assigntocomponent(x);). f. Όλες οι εντολές ανάθεσης που έχουν ως δεξί μέλος το πεδίο (π.χ. x= component;), θα τροποποιηθούν ώστε το δεξί μέλος να αντικατασταθεί με την κλήση της μεθόδου component.getreference();. Το ίδιο θα συμβεί και για τις εντολές δήλωσης που αρχικοποιούνται με το πεδίο. g. Όλες οι μέθοδοι της κλάσης Context που επιστρέφουν το πεδίο component, θα τροποποιηθούν ώστε να επιστρέφουν την κλήση της μεθόδου component.getreference(); h. Όλες οι κλήσεις μεθόδων που έχουν ως argument το πεδίο component, θα τροποποιηθούν ώστε να έχουν σαν argument την κλήση της μεθόδου component.getreference();. i. Όλες οι κλήσεις static μεθόδων της κλάσης Component που γίνονται από το πεδίο component (δηλαδή οι κλήσεις της μορφής component.staticmethod();) θα αντικατασταθούν με τις κλήσεις πάνω στην κλάση Component (δηλαδή, Component.staticMethod();). j. Εντοπίζονται όλες οι χρήσεις του τριαδικού τελεστή «?:» και ελέγχεται αν η έκφραση ελέγχου του περιέχει την έκφραση component!=null ή component ==null (ή κάποια από τις υπόλοιπες πιθανές μορφές ελέγχου αν το πεδίο είναι ίσο ή διάφορο του null). Σε περίπτωση που περιέχεται τέτοια έκφραση τότε αυτή αντικαθίσταται από τις εκφράσεις που περιγράφηκαν στα βήματα 6.d.i. και 6.d.ii πιο πάνω. 103
Εικόνα 26: Η κλάση Room πριν και μετά την εφαρμογή της αναδόμησης 104
4.5 Λεπτομέρειες Υλοποίησης Η προτεινόμενη μεθοδολογία αυτόματης ανίχνευσης και αναδόμησης προς το πρότυπο Null Object, που παρουσιάστηκε στις προηγούμενες ενότητες, ενσωματώθηκε στο plug-in JDeodorant του Eclipse. Η υλοποίηση της μεθοδολογίας βασίστηκε στην υπάρχουσα υλοποίηση του JDeodorant αξιοποιώντας μέρος της λειτουργικότητάς του. Η υλοποίηση που αναπτύχθηκε ενσωματώθηκε ως πρόσθετο κομμάτι του JDeodorant, χωρίς την τροποποίηση της υπάρχουσας λειτουργικότητας. Η λειτουργικότητα που προστέθηκε αναπτύχθηκε σχεδόν εξ ολοκλήρου σε καινούργιες κλάσεις, ενώ σε κάποιες περιπτώσεις απαιτήθηκε η επέκταση υπαρχουσών κλάσεων μέσω της προσθήκης νέας λειτουργικότητας. Πιο συγκεκριμένα, για τη συντακτική ανάλυση του κώδικα και τη δημιουργία του ΑST δέντρου χρησιμοποιήθηκε η υπάρχουσα υλοποίηση του JDeodorant, η οποία βασίζεται στις δυνατότητες που παρέχει η βιβλιοθήκη του Eclipse JDT [47], [48]. Για την υλοποίηση της προτεινόμενης μεθοδολογίας και την ανίχνευση των σημείων του κώδικα για την εφαρμογή του προτύπου, αξιοποιήθηκαν οι δυνατότητες της βιβλιοθήκης του Eclipse JDT, ενώ για το στάδιο της εκτέλεσης της αναδόμησης, οι δυνατότητες της βιβλιοθήκης JDT συνδυάστηκαν με την υποστήριξη της αναδόμησης της βιβλιοθήκης LTK του Eclipse [49], προκειμένου να ανασυνταχθεί το AST δέντρο και στη συνεχεία να τροποποιηθεί ο πηγαίος κώδικας βάσει του τροποποιημένου AST δέντρου. Επιπλέον, στην περίπτωση της εξέτασης των non-void μεθόδων, καθώς και της εξέτασης των τιμών των arguments του κατασκευαστή της exception κλάσης που δίδει μια throw εντολή (Βλ. και την ανάλυση της ενότητας 4.3.4.1), δημιουργήθηκαν οι απαιτούμενοι CFG και PDG γράφοι με αξιοποίηση της σχετικής υλοποίησης που είχε αναπτυχθεί από τους Τσάνταλη και Χατζηγεωργίου για τις απαιτήσεις της αναδόμησης εξαγωγής μεθόδου [46]. Βοηθητική ήταν και η χρήση του plug-in ASTViewer [40] για την επισκόπηση του AST δέντρου των υπό εξέταση συστημάτων λογισμικού που χρησιμοποιήθηκαν κατά τη φάση του ελέγχου της προτεινόμενης μεθοδολογίας (Βλ. επόμενο Κεφάλαιο 5). Στις επόμενες ενότητες του κεφαλαίου, παρουσιάζεται το γραφικό περιβάλλον του εργαλείου που υλοποιήθηκε και επιδεικνύεται η διαδικασία της ανίχνευσης των προτάσεων αναδόμησης, καθώς και της εφαρμογής της επιλεγμένης από το χρήστη αναδόμησης, μέσω ενός αντιπροσωπευτικού παραδείγματος που εντοπίστηκε στο 105
σύστημα λογισμικού Apache Ant [50]. Τέλος, στο Παράρτημα Α παρατίθενται συνοπτικά πληροφορίες σχετικά με την υλοποίηση της προτεινόμενης μεθοδολογίας και συγκεκριμένα τις τροποποιήσεις που απαιτήθηκαν στον υπάρχοντα κώδικα, καθώς και τις καινούργιες κλάσεις που δημιουργήθηκαν. Ενώ στο Παράρτημα Β δίνονται οδηγίες για την εγκατάσταση του plug-in JDeodorant. 4.6 Παρουσίαση του Εργαλείου Αναδόμησης Η μεθοδολογία ανίχνευσης και εκτέλεσης περιπτώσεων αναδόμησης προς το πρότυπο Null Object, όπως προαναφέρθηκε, ενσωματώθηκε ως πρόσθετη λειτουργικότητα στο plug-in JDeodorant του Eclipse. Αφού εγκατασταθεί το plug-in JDeodorant (Βλ. οδηγίες εγκατάστασης στο Παράρτημα Β), εμφανίζεται στη γραμμή μενού του περιβάλλοντος του Eclipse το μενού «Bad Smells». Μέσω του μενού «Bad Smells» ο χρήστης μπορεί να επιλέξει το σχεδιαστικό πρόβλημα, σχετικές με το οποίο περιπτώσεις επιθυμεί να εντοπιστούν στο υπό εξέταση project. Η επιλογή που προστέθηκε στο μενού «Bad Smells» και η οποία αφορά την αναδόμηση προς το πρότυπο Null Object ονομάστηκε «έλεγχοι για null (Null Checks)» (Βλ. Εικόνα 27). Αφού, λοιπόν, ο χρήστης επιλέξει, μέσω της όψης (view) Package Explorer, το project για το οποίο θέλει να ανιχνευτούν περιπτώσεις κατάλληλες για την εφαρμογή του προτύπου Null Object, στη συνέχεια πρέπει να επιλέξει Null Checks από το μενού Bad Smells. Τότε, στο παράθυρο (workbench) του περιβάλλοντος του Eclipse θα εμφανιστεί η όψη Null Checks μέσω της οποίας ο χρήστης έχει τη δυνατότητα να επιλέξει μέσω του κουμπιού «Identify Bad Smells» Object. την ανίχνευση των προτάσεων αναδόμησης προς το πρότυπο Null Οι υποψήφιες περιπτώσεις για αυτοματοποιημένη εφαρμογή της αναδόμησης προς το πρότυπο Null Object παρουσιάζονται στον χρήστη σε μορφή πίνακα και πάλι μέσω της όψης Null Checks. Επιπροσθέτως, μέσω της όψης Null Checks ο χρήστης έχει τη δυνατότητα να επιλέξει την αποθήκευση σε αρχείο κειμένου του πίνακα με τις υποψήφιες προτάσεις που ανιχνεύτηκαν, μέσω της σχετικής επιλογής «Save Results». 106
Εικόνα 27: Το μενού Bad Smells και η όψη Null Checks με τις επιλογές ανίχνευσης, αποθήκευσης και εφαρμογής των προτάσεων αναδόμησης προς το πρότυπο Null Object Οι προτάσεις που προκύπτουν ως αποτέλεσμα της διαδικασίας ανίχνευσης και εμφανίζονται στο χρήστη μέσω της όψης Null Checks, αντιστοιχούν στις υποθετικές δηλώσεις που η έκφρασή τους ελέγχει την τιμή κάποιου υποψήφιου για αναδόμηση πεδίου. Δηλαδή, στον πίνακα της όψης Null Checks ο χρήστης βλέπει για κάθε υποψήφιο προς αυτοματοποιημένη αναδόμηση προς το πρότυπο Null Object πεδίο, (δηλαδή για κάθε πεδίο που πληροί τις προϋποθέσεις της ενότητας 4.3.1 και δεν ανήκει σε κάποια από τις περιπτώσεις της παραγράφου 4.3.2), μια γραμμή για κάθε υποθετική δήλωση στην έκφραση της οποίας υπάρχει έλεγχος της τιμής του πεδίου. Για καθεμία τέτοια υποθετική δήλωση εμφανίζεται το πλήρες όνομα της κλάσης εντός της οποίας περιέχεται (στήλη Context Class), η δήλωση του υποψήφιου για αναδόμηση πεδίου που υπάρχει στην έκφραση της υποθετικής δήλωσης (στήλη Field Declaration), καθώς και αν η υποθετική δήλωση μπορεί να διαγραφεί (στήλη Condition can be removed), δηλαδή αν πληροί όλες τις προϋποθέσεις που περιγράφηκαν στην ενότητα 4.3.3. Τέλος, στη στήλη Refactoring Type αναγράφεται το πρότυπο που προτείνεται να εφαρμοστεί και έχει πάντα την τιμή «Εισαγωγή Null Object» (Introduce Null Object). Η στήλη αυτή προστέθηκε για λόγους συμβατότητας με τις προϋπάρχουσες όψεις του JDeodorant. Οι προτάσεις που προκύπτουν από τη διαδικασία της ανίχνευσης εμφανίζονται ταξινομημένες ανάλογα με το αν μπορούν να 107
διαγραφούν και ομαδοποιημένες ανά υποψήφιο πεδίο. Ο χρήστης, κάνοντας διπλό κλικ στη γραμμή του πίνακα της όψης Null Checks που περιέχει την υποθετική δήλωση που τον ενδιαφέρει, μπορεί να δει τη σχετική δήλωση. Συγκεκριμένα, εμφανίζεται στο χρήστη, στον επεξεργαστή του Eclipse, η κλάση εντός της οποίας βρίσκεται η υποθετική δήλωση, ενώ η υποθετική δήλωση προς αναδόμηση επισημαίνεται με διαφορετικό χρώμα από τον υπόλοιπο κώδικα, για τον ευκολότερο εντοπισμό της (Εικόνα 28). Ο χρήστης έχοντας εξετάσει τα κατάλληλα προς αναδόμηση πεδία που έχουν υποθετικές δηλώσεις υποψήφιες για διαγραφή, μπορεί για το πεδίο που το θεωρεί χρήσιμο να επιλέξει την αυτόματη εφαρμογή της αναδόμησης προς το πρότυπο Null Object. Για το σκοπό αυτό, αρκεί ο χρήστης να επιλέξει κάποια από τις υποθετικές δηλώσεις που αντιστοιχούν στο πεδίο, η οποία όμως να είναι υποψήφια για διαγραφή, και στη συνέχεια να κάνει κλικ στο κουμπί «Εφαρμογή της αναδόμησης (Apply Refactoring)». Η ενέργεια αυτή έχει ως αποτέλεσμα την εμφάνιση ενός οδηγού (wizard) που σχετίζεται με την εκτέλεση της αναδόμησης. Μέσω του οδηγού ο χρήστης μπορεί, καταρχάς, να επιθεωρήσει τα προεπιλεγμένα ονόματα που θα χρησιμοποιηθούν κατά τη δημιουργία των κλάσεων της ιεραρχίας και κατά τη δημιουργία των τυχόν assertnotnull{i} μεθόδων, με {i}=0, 1, 2,, αλλά και να προβεί στην τροποποίησή τους εφόσον το επιθυμεί (Εικόνα 29). Επίσης, μέσω του κουμπιού Preview του οδηγού, ανοίγει στο χρήστη η προεπισκόπηση του μετασχηματισμού που θα διενεργηθεί στον κώδικα κατά την εκτέλεση της αναδόμησης, ενώ μέσω των κουμπιών OK και Cancel μπορεί να προβεί σε αποδοχή ή απόρριψη της εφαρμογής της αναδόμησης αντίστοιχα (Εικόνα 30). Στην επόμενη ενότητα παρουσιάζονται λεπτομερώς οι μετασχηματισμοί που επιτελούνται κατά την εκτέλεση της αναδόμησης προς το πρότυπο Null Object μέσω ενός αντιπροσωπευτικού παραδείγματος που ανιχνεύτηκε σε ένα πραγματικό σύστημα λογισμικού. 108
Εικόνα 28: Παρουσίαση των προτάσεων αναδόμησης προς το πρότυπο Null Object και προεπισκόπηση της επιλεγμένης υποθετικής δήλωσης που ανιχνεύτηκε Εικόνα 29: Ο οδηγός για την προεπισκόπηση και τροποποίηση από το χρήστη των προεπιλεγμένων ονομάτων των κλάσεων και των μεθόδων που θα δημιουργηθούν 109
Εικόνα 30: Ο οδηγός για την προεπισκόπηση και αποδοχή ή απόρριψη του προτεινόμενου μετασχηματισμού 4.7 Παρουσίαση Εκτελούμενης Αναδόμησης Σε αυτήν την ενότητα θα παρουσιαστεί η εφαρμογή της αναδόμησης προς το πρότυπο Null Object, όπως δύναται να πραγματοποιηθεί αυτοματοποιημένα μέσω του εργαλείου που αναπτύχθηκε. Η επίδειξη της εφαρμογής της αναδόμησης θα πραγματοποιηθεί μέσω ενός αντιπροσωπευτικού παραδείγματος από το σύστημα λογισμικού Apache Ant [50], το οποίο παράδειγμα και ανιχνεύτηκε σαν υποψήφια περίπτωση για την αυτοματοποιημένη εφαρμογή του προτύπου. Μέσω του παραδείγματος επιχειρείται να καταστεί εμφανής η χρησιμότητα και η εφαρμοσιμότητα της αναδόμησης προς το πρότυπο Null Object σε πραγματικά συστήματα λογισμικού. Όπως φαίνεται στην Εικόνα 31, η διαδικασία ανίχνευσης υποψήφιων περιπτώσεων για την εφαρμογή του προτύπου Null Object που αναπτύχθηκε στα πλαίσια της εργασίας, εντόπισε στην context κλάση JUnitTestRunner το πεδίο με όνομα perm και τύπο Permissions ως κατάλληλο για την εφαρμογή του προτύπου. Η κλάση JUnitTestRunner αντιπροσωπεύει έναν απλό test runner για το framework JUnit, ο οποίος και εκτελεί όλες τις δοκιμασίες ελέγχου (tests) μιας σουίτας δοκιμασιών ελέγχου (test suite). Ενώ, το πεδίο perm αντιστοιχεί σε ένα σύνολο από permissions για τον test runner που εκτελεί το JUnit task. 110
Εικόνα 31: Το κατάλληλο για εφαρμογή του προτύπου Null Object πεδίο perm της κλάσης JUnitTestRunner με δυο υποθετικές δηλώσεις υποψήφιες για διαγραφή Το πεδίο perm όντως ικανοποιεί όλες τις προϋποθέσεις για να χαρακτηριστεί ως υποψήφιο για την εφαρμογή του προτύπου Null Object, μιας και ικανοποιεί όλες τις προϋποθέσεις της ενότητας 4.3.1, κάτι που επαληθεύεται τόσο από τη δήλωσή του (Βλ. Εικόνα 32), όσο και από την ύπαρξη ενός κατασκευαστή της κλάσης JUnitTestRunner ο οποίος δε θέτει το πεδίο (Βλ. Εικόνα 33), καθώς και γιατί δεν ανήκει σε καμία από τις περιπτώσεις που περιγράφηκαν στην ενότητα 4.3.2. Εικόνα 32: Η δήλωση του πεδίου perm πριν την εφαρμογή της αναδόμησης 111
Εικόνα 33: Κατασκευαστής της κλάσης JUnitTestRunner ο οποίος δε θέτει το πεδίο perm Όπως φαίνεται και από τα αποτελέσματα της ανίχνευσης της Εικόνας 31, για το πεδίο perm της κλάσης JUnitTestRunner εντοπίστηκαν συνολικά δύο υποθετικές δηλώσεις οι οποίες ελέγχουν αν το πεδίο είναι διάφορο του null. Και οι δύο υποθετικές δηλώσεις χαρακτηρίστηκαν ως υποψήφιες για διαγραφή. Επιλέγοντας την εκτέλεση της αναδόμησης, μια σειρά από μετασχηματισμούς του κώδικα του συστήματος πραγματοποιούνται. Καταρχάς, δημιουργείται η ιεραρχία κλάσεων η οποία αποτελείται από την κλάση με προτεινόμενο όνομα AbstractPerm (Βλ. Εικόνα 34) και από την υποκλάση αυτής που αντιστοιχεί στο Null Object και που έχει προτεινόμενο όνομα NullPerm (Βλ. Εικόνα 35). Επίσης, η κλάση του τύπου του πεδίου, Permissions, δηλώνεται ως υποκλάση της κλάσης AbstractPerm (Βλ. Εικόνα 36), και προστίθενται σε αυτήν οι μέθοδοι isnull() και getreference() (Βλ. Εικόνα 37). Ενώ, τέλος, τροποποιείται και η context κλάση JUnitTestRunner ως ακολούθως: τροποποιείται η δήλωση του πεδίου perm, το οποίο ορίζεται ως τύπου AbstractPerm και αρχικοποιείται με ένα στιγμιότυπο της κλάσης NullPerm (Βλ. Εικόνα 38), προστίθεται η μέθοδος assigntoperm (Βλ. Εικόνα 39) τροποποιούνται οι εντολές ανάθεσης που έχουν αριστερό μέλος το πεδίο perm 112
ώστε πλέον να έχουν ως δεξί μέλος την κλήση της μεθόδου assigntoperm (Βλ. Εικόνα 40) και τέλος, οι δύο υποθετικές δηλώσεις που ήταν υποψήφιες για διαγραφή αντικαθίστανται με τις εντολές που βρίσκονταν στο then σώμα τους (Βλ. Εικόνα 40). Εικόνα 34: Η δημιουργηθείσα κλάση AbstractPerm Εικόνα 35: Η δημιουργηθείσα κλάση NullPerm 113
Εικόνα 36: Η κλάση Permissions που αντιστοιχεί στον τύπο του πεδίου δηλώνεται ως υποκλάση της κλάσης AbstractPerm Εικόνα 37: Η προσθήκη των μεθόδων isnull() και getreference() στην κλάση Permissions 114
Εικόνα 38: Η τροποποίηση της δήλωσης του πεδίου perm στην context κλάση JUnitTestRunner Εικόνα 39: Η προσθήκη της μεθόδου assigntoperm στην context κλάση JUnitTestRunner 115
Εικόνα 40: Περίπτωση εντολής ανάθεσης που τροποποιήθηκε, καθώς και υποθετικής λογικής που διαγράφηκε 4.8 Συμπεράσματα και Περιορισμοί Ολοκληρώνοντας την παρουσίαση της προτεινόμενης μεθοδολογίας κρίνεται αναγκαίο να επισημανθούν οι περιορισμοί της προσέγγισης που αναπτύχθηκε. Η προτεινόμενη μεθοδολογία ανίχνευσης περιπτώσεων για αυτοματοποιημένη εφαρμογή της αναδόμησης προς το πρότυπο Null Object έχει οριοθετηθεί πολύ αυστηρά με αποτέλεσμα να μην τίθεται αμφισβήτηση για την ορθότητα των προτεινόμενων προτάσεων αναδόμησης που ανιχνεύει το εργαλείο. Επιπλέον, ιδιαίτερη ήταν η μέριμνα ώστε κατά την εκτέλεση της αναδόμησης να μην εισάγονται λάθη, αλλά και να διατηρείται ίδια η συμπεριφορά του συστήματος. Ο πιο σημαντικός περιορισμός της μεθοδολογίας θα λέγαμε ότι είναι η αδυναμία της να ανιχνεύσει ως υποψήφιες περιπτώσεις για αυτοματοποιημένη εκτέλεση της αναδόμησης αυτές που αφορούν σε πεδία που ανήκουν σε κάποιες από τις περιπτώσεις που αναλύθηκαν στην ενότητα 4.3.2. Οι περιπτώσεις αυτές απαιτούν να γίνουν εκτεταμένες τροποποιήσεις στο υπό εξέταση σύστημα λογισμικού και δυσχεραίνουν πολύ την εφαρμογή της αναδόμησης. Στις περισσότερες από αυτές τις περιπτώσεις κρίνεται απαραίτητη η παρέμβαση του προγραμματιστή διότι είναι πολύ επίπονο να εφαρμοστεί αυτοματοποιημένα η αναδόμηση και να εξασφαλιστεί 116