Προγραμματισμός

Χρησιμοποιήστε σταθερούς τύπους για ασφαλέστερο και καθαρότερο κώδικα

Σε αυτό το σεμινάριο θα επεκταθεί η ιδέα του απαριθμημένες σταθερές όπως καλύπτεται από τον Eric Armstrong, "Δημιουργήστε απαριθμημένες σταθερές στην Java." Συνιστώ ανεπιφύλακτα να διαβάσετε αυτό το άρθρο προτού μπείτε σε αυτό, καθώς θα υποθέσω ότι είστε εξοικειωμένοι με τις έννοιες που σχετίζονται με τις απαριθμημένες σταθερές και θα επεκταθώ σε κάποιο από τα παραδείγματα κώδικα που παρουσίασε ο Eric.

Η έννοια των σταθερών

Αντιμετωπίζοντας τις απαριθμημένες σταθερές, θα συζητήσω το απαριθμημένος μέρος της έννοιας στο τέλος του άρθρου. Προς το παρόν, θα επικεντρωθούμε μόνο στο συνεχής άποψη. Οι σταθερές είναι βασικά μεταβλητές των οποίων η τιμή δεν μπορεί να αλλάξει. Στο C / C ++, η λέξη-κλειδί υπ χρησιμοποιείται για να δηλώσει αυτές τις σταθερές μεταβλητές. Στην Java, χρησιμοποιείτε τη λέξη-κλειδί τελικός. Ωστόσο, το εργαλείο που εισάγεται εδώ δεν είναι απλώς μια πρωτόγονη μεταβλητή. είναι μια πραγματική παρουσία αντικειμένου. Οι παρουσίες αντικειμένων είναι αμετάβλητες και αμετάβλητες - η εσωτερική τους κατάσταση ενδέχεται να μην τροποποιηθεί. Αυτό είναι παρόμοιο με το μοτίβο singleton, όπου μια τάξη μπορεί να έχει μόνο μία παρουσία. σε αυτήν την περίπτωση, ωστόσο, μια τάξη μπορεί να έχει μόνο ένα περιορισμένο και προκαθορισμένο σύνολο παρουσιών.

Οι κύριοι λόγοι για τη χρήση σταθερών είναι η σαφήνεια και η ασφάλεια. Για παράδειγμα, το ακόλουθο κομμάτι κώδικα δεν είναι αυτονόητο:

 public void setColor (int x) {...} public void someMethod () {setColor (5); } 

Από αυτόν τον κωδικό, μπορούμε να βεβαιωθούμε ότι έχει οριστεί ένα χρώμα. Αλλά τι χρώμα αντιπροσωπεύει το 5; Εάν αυτός ο κωδικός γράφτηκε από έναν από τους σπάνιους προγραμματιστές που σχολιάζει το έργο του, ενδέχεται να βρούμε την απάντηση στην κορυφή του αρχείου. Αλλά πιθανότατα θα πρέπει να ψάξουμε για κάποια παλιά έγγραφα σχεδιασμού (αν υπάρχουν ακόμη) για μια εξήγηση.

Μια πιο ξεκάθαρη λύση είναι να αντιστοιχίσετε μια τιμή 5 σε μια μεταβλητή με ένα νόημα όνομα. Για παράδειγμα:

 δημόσιο στατικό τελικό int RED = 5; public void someMethod () {setColor (RED). } 

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

δημόσιο στατικό τελικό int RED = 3; δημόσιο στατικό τελικό ΠΡΑΣΙΝΟ = 5; 

Τώρα έχουμε δύο προβλήματα. Πρωτα απο ολα, ΤΟ ΚΟΚΚΙΝΟ δεν έχει οριστεί πλέον στη σωστή τιμή. Δεύτερον, η τιμή για το κόκκινο αντιπροσωπεύεται από τη μεταβλητή που ονομάζεται ΠΡΑΣΙΝΟΣ. Ίσως το πιο τρομακτικό μέρος είναι ότι αυτός ο κώδικας θα μεταγλωττιστεί καλά, και το σφάλμα ενδέχεται να μην ανιχνευθεί μέχρι να αποσταλεί το προϊόν.

Μπορούμε να διορθώσουμε αυτό το πρόβλημα δημιουργώντας μια οριστική κατηγορία χρωμάτων:

δημόσια τάξη Χρώμα {public static final int RED = 5; δημόσιο στατικό τελικό int GREEN = 7; } 

Στη συνέχεια, μέσω τεκμηρίωσης και ελέγχου κώδικα, ενθαρρύνουμε τους προγραμματιστές να το χρησιμοποιούν έτσι:

 public void someMethod () {setColor (Color.RED); } 

Λέω ενθαρρύνω γιατί ο σχεδιασμός σε αυτήν την λίστα κωδικών δεν μας επιτρέπει να αναγκάσουμε τον κωδικοποιητή να συμμορφωθεί. ο κώδικας θα μεταγλωττιστεί ακόμα κι αν όλα δεν είναι αρκετά σωστά. Έτσι, ενώ αυτό είναι λίγο ασφαλέστερο, δεν είναι απολύτως ασφαλές. Αν και προγραμματιστές πρέπει Χρησιμοποιήστε το Χρώμα τάξη, δεν απαιτείται. Οι προγραμματιστές θα μπορούσαν πολύ εύκολα να γράψουν και να συντάξουν τον ακόλουθο κώδικα:

 setColor (3498910); 

Κάνει το setColor μέθοδος αναγνωρίζει αυτόν τον μεγάλο αριθμό να είναι ένα χρώμα; Πιθανώς όχι. Πώς μπορούμε λοιπόν να προστατευθούμε από αυτούς τους απατεώνες προγραμματιστές; Εκεί έρχονται οι σωστικοί τύποι για τη διάσωση.

Ξεκινάμε επαναπροσδιορίζοντας την υπογραφή της μεθόδου:

 public void setColor (Χρώμα x) {...} 

Τώρα οι προγραμματιστές δεν μπορούν να περάσουν σε μια αυθαίρετη ακέραια τιμή. Αναγκάζονται να παράσχουν ένα έγκυρο Χρώμα αντικείμενο. Ένα παράδειγμα εφαρμογής αυτού μπορεί να μοιάζει με αυτό:

 public void someMethod () {setColor (νέο χρώμα ("κόκκινο")); } 

Εργαζόμαστε ακόμα με καθαρό, αναγνώσιμο κώδικα και είμαστε πολύ πιο κοντά στην επίτευξη απόλυτης ασφάλειας. Αλλά δεν είμαστε ακόμα αρκετά εκεί. Ο προγραμματιστής έχει ακόμα αρκετό χώρο για να καταστρέψει τον όλεθρο και μπορεί αυθαίρετα να δημιουργήσει νέα χρώματα όπως έτσι:

 public void someMethod () {setColor (νέο χρώμα ("Γεια, το όνομά μου είναι Ted.")); } 

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

δημόσια τάξη Χρώμα {ιδιωτικό χρώμα () {} δημόσιο στατικό τελικό Χρώμα ΚΟΚΚΙΝΟ = νέο χρώμα (); δημόσιο στατικό τελικό Χρώμα ΠΡΑΣΙΝΟ = νέο χρώμα (); δημόσιο στατικό τελικό Χρώμα ΜΠΛΕ = νέο Χρώμα (); } 

Σε αυτόν τον κώδικα επιτύχαμε επιτέλους την απόλυτη ασφάλεια. Ο προγραμματιστής δεν μπορεί να κατασκευάσει ψεύτικα χρώματα. Μπορούν να χρησιμοποιηθούν μόνο τα καθορισμένα χρώματα. Διαφορετικά, το πρόγραμμα δεν θα μεταγλωττιστεί. Έτσι φαίνεται η εφαρμογή μας:

 public void someMethod () {setColor (Color.RED); } 

επιμονή

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

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

Οι χορδές, ωστόσο, μπορεί να είναι ακριβές για την αποθήκευση. Ένας ακέραιος αριθμός απαιτεί 32 bit για να αποθηκεύσει την τιμή του, ενώ μια συμβολοσειρά απαιτεί 16 bit ανά χαρακτήρα (λόγω της υποστήριξης Unicode). Για παράδειγμα, ο αριθμός 49858712 μπορεί να αποθηκευτεί σε 32 bit, αλλά η συμβολοσειρά ΤΟΥΡΚΟΥΑΖ θα απαιτούσε 144 bits. Εάν αποθηκεύετε χιλιάδες αντικείμενα με χαρακτηριστικά χρώματος, αυτή η σχετικά μικρή διαφορά σε bit (μεταξύ 32 και 144 σε αυτήν την περίπτωση) μπορεί να αυξηθεί γρήγορα. Ας χρησιμοποιήσουμε λοιπόν ακέραιες τιμές. Ποια είναι η λύση σε αυτό το πρόβλημα; Θα διατηρήσουμε τις τιμές συμβολοσειράς, επειδή είναι σημαντικές για παρουσίαση, αλλά δεν θα τις αποθηκεύσουμε.

Οι εκδόσεις Java από 1.1 και μετά είναι σε θέση να σειριοποιούν αυτόματα τα αντικείμενα, αρκεί να εφαρμόζουν το Σειριοποιήσιμο διεπαφή. Για να αποτρέψετε την αποθήκευση ξένων δεδομένων από την Java, πρέπει να δηλώσετε τέτοιες μεταβλητές με το παροδικός λέξη-κλειδί. Έτσι, για να αποθηκεύσουμε τις ακέραιες τιμές χωρίς να αποθηκεύσουμε την παράσταση συμβολοσειράς, δηλώνουμε το χαρακτηριστικό συμβολοσειράς ως παροδικό. Εδώ είναι η νέα τάξη, μαζί με τους αξεσουάρ στα ακέραια και τα χαρακτηριστικά συμβολοσειρών:

δημόσια κλάση Color εφαρμόζει java.io.Serializable {private int value; ιδιωτικό προσωρινό όνομα συμβολοσειράς; δημόσιο στατικό τελικό Χρώμα ΚΟΚΚΙΝΟ = νέο χρώμα (0, "Κόκκινο"); δημόσιο στατικό τελικό Χρώμα ΜΠΛΕ = νέο Χρώμα (1, "Μπλε"); δημόσια στατική τελική Χρώμα ΠΡΑΣΙΝΟ = νέο χρώμα (2, "Πράσινο"); ιδιωτικό χρώμα (τιμή int, όνομα συμβολοσειράς) {this.value = value; this.name = όνομα; } δημόσια int getValue () {τιμή επιστροφής; } δημόσια συμβολοσειρά toString () {όνομα επιστροφής; }} 

Τώρα μπορούμε να αποθηκεύσουμε αποτελεσματικά παρουσίες σταθερού τύπου Χρώμα. Τι γίνεται όμως με την αποκατάστασή τους; Αυτό θα είναι λίγο δύσκολο. Προτού προχωρήσουμε περισσότερο, ας το επεκτείνουμε σε ένα πλαίσιο που θα χειριστεί όλες τις προαναφερθείσες παγίδες για εμάς, επιτρέποντάς μας να επικεντρωθούμε στο απλό θέμα του ορισμού των τύπων.

Το πλαίσιο σταθερού τύπου

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

δημόσια τάξη Το χρώμα επεκτείνεται Τύπος {προστατευμένο χρώμα (int τιμή, συμβολοσειρά desc) {super (value, desc); } δημόσιο στατικό τελικό Χρώμα ΚΟΚΚΙΝΟ = νέο χρώμα (0, "Κόκκινο"); δημόσιο στατικό τελικό Χρώμα ΜΠΛΕ = νέο Χρώμα (1, "Μπλε"); δημόσιο στατικό τελικό ΧΡΩΜΑ = νέο χρώμα (2, "Πράσινο"); } 

ο Χρώμα Η τάξη δεν περιλαμβάνει τίποτα άλλο από έναν κατασκευαστή και μερικές εμφανιζόμενες στο κοινό περιπτώσεις. Όλη η λογική που συζητήθηκε σε αυτό το σημείο θα καθοριστεί και θα εφαρμοστεί στο superclass Τύπος; θα προσθέσουμε περισσότερα καθώς προχωράμε. Εδώ είναι τι Τύπος μοιάζει μέχρι τώρα:

δημόσιος τύπος τύπου υλοποιεί java.io.Serializable {private int value; ιδιωτικό προσωρινό όνομα συμβολοσειράς; προστατευμένος τύπος (τιμή int, όνομα συμβολοσειράς) {this.value = value; this.name = όνομα; } δημόσια int getValue () {τιμή επιστροφής; } δημόσια συμβολοσειρά toString () {όνομα επιστροφής; }} 

Επιστροφή στην επιμονή

Με το νέο μας πλαίσιο, μπορούμε να συνεχίσουμε εκεί που σταματήσαμε στη συζήτηση της επιμονής. Θυμηθείτε, μπορούμε να σώσουμε τους τύπους μας αποθηκεύοντας τις ακέραιες τιμές τους, αλλά τώρα θέλουμε να τους επαναφέρουμε. Αυτό θα απαιτήσει ένα ψάχνω - έναν αντίστροφο υπολογισμό για τον εντοπισμό της παρουσίας αντικειμένου με βάση την τιμή του. Για να πραγματοποιήσουμε μια αναζήτηση, χρειαζόμαστε έναν τρόπο να απαριθμήσουμε όλους τους πιθανούς τύπους.

Στο άρθρο του Eric, εφάρμοσε τη δική του απαρίθμηση εφαρμόζοντας τις σταθερές ως κόμβους σε μια συνδεδεμένη λίστα. Θα παραιτηθώ από αυτήν την πολυπλοκότητα και θα χρησιμοποιήσω ένα απλό hashtable. Το κλειδί για το κατακερματισμό θα είναι οι ακέραιες τιμές του τύπου (τυλιγμένες σε ένα Ακέραιος αριθμός αντικείμενο) και η τιμή του κατακερματισμού θα είναι αναφορά στην παρουσία τύπου. Για παράδειγμα, το ΠΡΑΣΙΝΟΣ παρουσία του Χρώμα θα αποθηκευτεί έτσι:

 hashtable.put (νέο Integer (GREEN.getValue ()), GREEN); 

Φυσικά, δεν θέλουμε να το πληκτρολογήσουμε για κάθε πιθανό τύπο. Θα μπορούσαν να υπάρχουν εκατοντάδες διαφορετικές τιμές, δημιουργώντας έτσι έναν εφιάλτη δακτυλογράφησης και ανοίγοντας τις πόρτες σε κάποια δυσάρεστα προβλήματα - ίσως ξεχάσετε να βάλετε μια από τις τιμές στο hashtable και στη συνέχεια να μην μπορείτε να το αναζητήσετε αργότερα, για παράδειγμα. Έτσι θα δηλώσουμε ένα παγκόσμιο hashtable μέσα Τύπος και τροποποιήστε τον κατασκευαστή για να αποθηκεύσετε τη χαρτογράφηση κατά τη δημιουργία:

 private static final Hashtable types = νέο Hashtable (); προστατευμένος τύπος (int τιμή, συμβολοσειρά desc) {this.value = value; αυτό.desc = desc; types.put (νέο ακέραιο (τιμή), αυτό); } 

Αυτό όμως δημιουργεί πρόβλημα. Εάν έχουμε μια υποκατηγορία που ονομάζεται Χρώμα, ο οποίος έχει έναν τύπο (δηλαδή, Πράσινος) με τιμή 5 και στη συνέχεια δημιουργούμε μια άλλη υποκατηγορία που ονομάζεται Απόχρωση, το οποίο έχει επίσης έναν τύπο (δηλαδή Σκοτάδι) με τιμή 5, μόνο ένα από αυτά θα αποθηκευτεί στο hashtable - το τελευταίο που θα δημιουργηθεί.

Για να το αποφύγουμε αυτό, πρέπει να αποθηκεύσουμε μια λαβή στον τύπο βάσει όχι μόνο της αξίας του, αλλά και του τάξη. Ας δημιουργήσουμε μια νέα μέθοδο για την αποθήκευση των αναφορών τύπου. Θα χρησιμοποιήσουμε ένα hashtable hashtables. Το εσωτερικό hashtable θα είναι μια αντιστοίχιση τιμών σε τύπους για κάθε συγκεκριμένη υποκατηγορία (Χρώμα, Απόχρωση, και ούτω καθεξής). Το εξωτερικό hashtable θα είναι μια χαρτογράφηση υποκατηγοριών σε εσωτερικούς πίνακες.

Αυτή η ρουτίνα θα προσπαθήσει πρώτα να αποκτήσει τον εσωτερικό πίνακα από τον εξωτερικό πίνακα. Εάν λάβει ένα μηδέν, ο εσωτερικός πίνακας δεν υπάρχει ακόμα. Έτσι, δημιουργούμε ένα νέο εσωτερικό τραπέζι και το βάζουμε στον εξωτερικό πίνακα. Στη συνέχεια, προσθέτουμε την αντιστοίχιση τιμής / τύπου στον εσωτερικό πίνακα και τελειώσαμε. Εδώ είναι ο κωδικός:

 private void storeType (Type type) {String className = type.getClass (). getName (); Τιμές Hashtable; synchronized (types) // αποφύγετε την κατάσταση του αγώνα για τη δημιουργία εσωτερικού πίνακα {values ​​= (Hashtable) types.get (className); if (τιμές == null) {τιμές = νέο Hashtable (); types.put (className, τιμές); }} Values.put (νέο Integer (type.getValue ()), type); } 

Και εδώ είναι η νέα έκδοση του κατασκευαστή:

 προστατευμένος τύπος (int τιμή, String desc) {this.value = value; αυτό.desc = desc; storeType (αυτό); } 

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

 δημόσια στατική Τύπος getByValue (Class classRef, int value) {Type type = null; Συμβολοσειρά className = classRef.getName (); Τιμές Hashtable = (Hashtable) types.get (className); if (τιμές! = null) {type = (Type) values.get (new Integer (value)); } επιστροφή (τύπος); } 

Επομένως, η επαναφορά μιας τιμής είναι τόσο απλή όσο αυτή (σημειώστε ότι η τιμή επιστροφής πρέπει να μετατραπεί):

 int value = // read from file, database, etc. Χρώμα φόντου = (ColorType) Type.findByValue (ColorType.class, value); 

Καταμέτρηση των τύπων

Χάρη στον οργανισμό μας hashtable-of-hashtables, είναι απίστευτα απλό να εκθέσουμε τη λειτουργικότητα απαρίθμησης που προσφέρει η υλοποίηση του Eric. Η μόνη προειδοποίηση είναι ότι η διαλογή, που προσφέρει ο σχεδιασμός του Eric, δεν είναι εγγυημένη. Εάν χρησιμοποιείτε Java 2, μπορείτε να αντικαταστήσετε τον ταξινομημένο χάρτη για τα εσωτερικά hashtables. Όμως, όπως ανέφερα στην αρχή αυτής της στήλης, ενδιαφέρομαι μόνο για την έκδοση 1.1 του JDK αυτήν τη στιγμή.

Η μόνη λογική που απαιτείται για την απαρίθμηση των τύπων είναι η ανάκτηση του εσωτερικού πίνακα και η επιστροφή της λίστας στοιχείων. Εάν ο εσωτερικός πίνακας δεν υπάρχει, επιστρέφουμε απλώς null. Εδώ είναι ολόκληρη η μέθοδος: