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

Γιατί η επέκταση είναι κακό

ο εκτείνεται η λέξη-κλειδί είναι κακό; ίσως όχι στο επίπεδο του Charles Manson, αλλά αρκετά κακό που πρέπει να αποφεύγεται όποτε είναι δυνατόν. Η συμμορία των τεσσάρων Σχεδιαστικά πρότυπα Το βιβλίο συζητά επιτέλους την αντικατάσταση της κληρονομιάς εφαρμογήςεκτείνεται) με κληρονομιά διεπαφής (υλοποιεί).

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

Διεπαφές έναντι τάξεων

Παρευρέθηκα κάποτε σε μια συνάντηση ομάδας χρηστών της Java όπου ο James Gosling (εφευρέτης της Java) ήταν ο επιλεγμένος ομιλητής. Κατά τη διάρκεια της αξέχαστης συνάντησης ερωτήσεων και απαντήσεων, κάποιος τον ρώτησε: "Εάν μπορούσατε να κάνετε ξανά Java, τι θα άλλαζες;" «Θα φύγω μαθήματα», απάντησε. Αφού το γέλιο πέθανε, εξήγησε ότι το πραγματικό πρόβλημα δεν ήταν τα μαθήματα καθαυτά, αλλά μάλλον η κληρονομιά εφαρμογής (το εκτείνεται σχέση). Κληρονομιά διεπαφής (το υλοποιεί προτιμάται). Θα πρέπει να αποφεύγετε την εφαρμογή κληρονομιάς όποτε είναι δυνατόν.

Χάνοντας ευελιξία

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

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

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

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

f () {LinkedList list = new LinkedList (); //... g (λίστα); } g (Λίστα LinkedList) {list.add (...); g2 (λίστα)} 

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

Επανεγγραφή του κώδικα ως εξής:

f () {Λίστα συλλογής = νέα LinkedList (); //... g (λίστα); } g (Λίστα συλλογών) {list.add (...); g2 (λίστα)} 

καθιστά δυνατή την αλλαγή της συνδεδεμένης λίστας σε έναν πίνακα κατακερματισμού απλώς αντικαθιστώντας το νέα LinkedList () με νέο HashSet (). Αυτό είναι. Δεν απαιτούνται άλλες αλλαγές.

Ως άλλο παράδειγμα, συγκρίνετε αυτόν τον κωδικό:

f () {Συλλογή c = νέο HashSet (); //... g (γ); } g (Συλλογή c) {για (Iterator i = c.iterator (); i.hasNext ();) do_something_with (i.next ()); } 

σε αυτό:

f2 () {Συλλογή c = νέο HashSet (); //... g2 (c.iterator ()); } g2 (Iterator i) {while (i.hasNext ();) do_something_with (i.next ()); } 

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

Σύζευξη

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

Ως σχεδιαστής, πρέπει να προσπαθήσετε να ελαχιστοποιήσετε τις σχέσεις ζεύξης. Δεν μπορείτε να εξαλείψετε εντελώς τη σύζευξη επειδή μια μέθοδος κλήσης από ένα αντικείμενο μιας κλάσης σε ένα αντικείμενο μιας άλλης είναι μια μορφή χαλαρής ζεύξης. Δεν μπορείτε να έχετε ένα πρόγραμμα χωρίς κάποια σύνδεση. Παρ 'όλα αυτά, μπορείτε να ελαχιστοποιήσετε τη σύζευξη, ακολουθώντας με σιγουριά ακολουθώντας τις εντολές OO (αντικειμενοστρεφείς) (το πιο σημαντικό είναι ότι η υλοποίηση ενός αντικειμένου πρέπει να είναι εντελώς κρυμμένη από τα αντικείμενα που το χρησιμοποιούν). Για παράδειγμα, οι μεταβλητές παρουσίας ενός αντικειμένου (πεδία μέλους που δεν είναι σταθερές), θα πρέπει πάντα να είναι ιδιωτικός. Περίοδος. Χωρίς εξαιρέσεις. Πάντα. Το εννοώ. (Μπορείτε περιστασιακά να χρησιμοποιήσετε προστατευμένο αποτελεσματικές μεθόδους, αλλά προστατευμένο οι μεταβλητές παρουσίας είναι μια αηδία.) Δεν πρέπει ποτέ να χρησιμοποιείτε συναρτήσεις get / set για τον ίδιο λόγο - είναι απλώς υπερβολικά περίπλοκοι τρόποι δημοσίευσης ενός πεδίου (αν και οι λειτουργίες πρόσβασης που επιστρέφουν πλήρη αντικείμενα αντί για μια τιμή βασικού τύπου είναι εύλογο σε καταστάσεις όπου η κλάση του επιστρεφόμενου αντικειμένου αποτελεί βασική αφαίρεση στο σχεδιασμό).

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

Το εύθραυστο βασικό πρόβλημα

Τώρα, ας εφαρμόσουμε την έννοια της σύζευξης στην κληρονομιά. Σε ένα σύστημα υλοποίησης-κληρονομιάς που χρησιμοποιεί εκτείνεται, οι παράγωγες τάξεις συνδέονται πολύ στενά με τις βασικές κατηγορίες, και αυτή η στενή σύνδεση είναι ανεπιθύμητη. Οι σχεδιαστές έχουν εφαρμόσει το moniker "το εύθραυστο βασικό πρόβλημα" για να περιγράψουν αυτήν τη συμπεριφορά. Οι βασικές κατηγορίες θεωρούνται εύθραυστες επειδή μπορείτε να τροποποιήσετε μια βασική τάξη με έναν φαινομενικά ασφαλή τρόπο, αλλά αυτή η νέα συμπεριφορά, όταν κληρονομείται από τις παράγωγες τάξεις, μπορεί να προκαλέσει δυσλειτουργία των παραγόμενων τάξεων. Δεν μπορείτε να πείτε εάν μια αλλαγή βασικής τάξης είναι ασφαλής απλά εξετάζοντας τις μεθόδους της βασικής τάξης μεμονωμένα. πρέπει να εξετάσετε (και να δοκιμάσετε) όλες τις παραγόμενες τάξεις επίσης. Επιπλέον, πρέπει να ελέγξετε όλο τον κώδικα χρήσεις και οι δύο βασικές κατηγορίες και παράγωγα αντικείμενα κλάσης, καθώς αυτός ο κώδικας μπορεί επίσης να σπάσει από τη νέα συμπεριφορά. Μια απλή αλλαγή σε βασική κλάση βάσης μπορεί να καταστήσει ένα ολόκληρο πρόγραμμα ακατάλληλο.

Ας εξετάσουμε μαζί τα εύθραυστα προβλήματα σύζευξης βάσης και βάσης. Η ακόλουθη τάξη επεκτείνει τα Java Λίστα Array τάξη για να συμπεριφέρεται σαν στοίβα:

Η κλάση Stack επεκτείνει το ArrayList {private int stack_pointer = 0; δημόσια άκυρη ώθηση (αντικείμενο αντικειμένου) {add (stack_pointer ++, article); } δημόσιο αναδυόμενο αντικείμενο () {return remove (--stack_pointer); } public void push_many (Object [] άρθρα) {για (int i = 0; i <άρθρα.length; ++ i) push (άρθρα [i]); }} 

Ακόμα και μια τάξη τόσο απλή όσο αυτή έχει προβλήματα. Σκεφτείτε τι συμβαίνει όταν ένας χρήστης αξιοποιεί την κληρονομιά και χρησιμοποιεί το Λίστα Array'μικρό Σαφή() μέθοδος για να ξεπεράσετε τα πάντα από τη στοίβα:

Στοίβα a_stack = νέα στοίβα (); a_stack.push ("1"); a_stack.push ("2"); a_stack.clear (); 

Ο κωδικός μεταγλωττίζεται με επιτυχία, αλλά επειδή η βασική κλάση δεν γνωρίζει τίποτα για το δείκτη στοίβας, το Σωρός το αντικείμενο είναι τώρα σε απροσδιόριστη κατάσταση. Η επόμενη κλήση προς Σπρώξτε() βάζει το νέο στοιχείο στο ευρετήριο 2 (το stack_pointerτρέχουσα τιμή), οπότε η στοίβα έχει ουσιαστικά τρία στοιχεία πάνω της - τα δύο κάτω είναι σκουπίδια. (Java) Σωρός η τάξη έχει ακριβώς αυτό το πρόβλημα. μην το χρησιμοποιείς.)

Μια λύση στο ανεπιθύμητο πρόβλημα κληρονομικής μεθόδου είναι Σωρός για παράκαμψη όλων Λίστα Array μεθόδους που μπορούν να τροποποιήσουν την κατάσταση του πίνακα, επομένως οι παρακάμψεις είτε χειρίζονται σωστά το δείκτη στοίβας είτε ρίχνουν μια εξαίρεση. (Ο αφαίρεση εύρους () Η μέθοδος είναι ένας καλός υποψήφιος για την εξαίρεση.)

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

Μια καλύτερη λύση στο ζήτημα της βασικής κατηγορίας είναι η ενσωμάτωση της δομής δεδομένων αντί της χρήσης κληρονομιάς. Ακολουθεί μια νέα και βελτιωμένη έκδοση του Σωρός:

class Stack {ιδιωτικό int stack_pointer = 0; ιδιωτικό ArrayList the_data = νέο ArrayList (); δημόσια άκυρη ώθηση (άρθρο αντικειμένου) {the_data.add (stack_pointer ++, άρθρο); } δημόσιο αναδυόμενο αντικείμενο () {return the_data.remove (--stack_pointer); } public void push_many (Object [] άρθρα) {για (int i = 0; i <o.length; ++ i) push (άρθρα [i]); }} 

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

Η κλάση Monitorable_stack επεκτείνει το Stack {private int high_water_mark = 0; ιδιωτικό int current_size; δημόσια άκυρη ώθηση (άρθρο αντικειμένου) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (άρθρο); } δημόσιο αναδυόμενο αντικείμενο () {--current_size; επιστροφή super.pop (); } public int max_size_so_far () {return high_water_mark; }} 

Αυτή η νέα τάξη λειτουργεί καλά, τουλάχιστον για λίγο. Δυστυχώς, ο κώδικας εκμεταλλεύεται το γεγονός ότι push_many () κάνει τη δουλειά του καλώντας Σπρώξτε(). Στην αρχή, αυτή η λεπτομέρεια δεν φαίνεται κακή επιλογή. Απλοποιεί τον κώδικα και λαμβάνετε την παραγόμενη έκδοση κλάσης του Σπρώξτε(), ακόμη και όταν το Παρακολούθηση_stack είναι προσβάσιμο μέσω a Σωρός αναφορά, έτσι το high_water_mark ενημερώσεις σωστά.

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

class Stack {ιδιωτικό int stack_pointer = -1; ιδιωτικό αντικείμενο [] στοίβα = νέο αντικείμενο [1000]; δημόσια άκυρη ώθηση (άρθρο αντικειμένου) {assert stack_pointer = 0; επιστροφή στοίβας [stack_pointer--]; } public void push_many (Object [] άρθρα) {assert (stack_pointer + άρθρα.length) <stack.length; System.arraycopy (άρθρα, 0, stack, stack_pointer + 1, άρθρα.length); stack_pointer + = άρθρα. μήκος; }} 

Σημειώσε ότι push_many () όχι πλέον κλήσεις Σπρώξτε() πολλές φορές — πραγματοποιεί μεταφορά μπλοκ. Η νέα έκδοση του Σωρός δουλεύει μια χαρά; στην πραγματικότητα, είναι καλύτερα από την προηγούμενη έκδοση. Δυστυχώς, το Παρακολούθηση_stack παράγωγη τάξη όχι να δουλεύει πια, καθώς δεν θα παρακολουθεί σωστά τη χρήση στοίβας εάν push_many () ονομάζεται (η έκδοση της παραγόμενης κατηγορίας του Σπρώξτε() δεν καλείται πλέον από τους κληρονόμους push_many () μέθοδος, έτσι push_many () δεν ενημερώνει πλέον το high_water_mark). Σωρός είναι μια εύθραυστη βασική κατηγορία. Όπως αποδεικνύεται, είναι σχεδόν αδύνατο να εξαλειφθούν αυτά τα είδη προβλημάτων απλώς με προσοχή.

Λάβετε υπόψη ότι δεν έχετε αυτό το πρόβλημα εάν χρησιμοποιείτε την κληρονομιά διεπαφής, καθώς δεν υπάρχει κληρονομική λειτουργικότητα για να σας κάνει κακό. Αν Σωρός είναι μια διεπαφή, που εφαρμόζεται και από τα δύο a Απλή στοίβα και ένα Παρακολούθηση_stack, τότε ο κώδικας είναι πολύ πιο ισχυρός.