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

Συμβουλή Java 75: Χρησιμοποιήστε ένθετα μαθήματα για καλύτερη οργάνωση

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

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

(Ο πλήρης πηγαίος κώδικας για αυτήν τη συμβουλή μπορεί να ληφθεί σε μορφή zip από την ενότητα Πόροι.)

Ένθετα μαθήματα έναντι εσωτερικών τάξεων

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

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

Κίνητρο

Εξετάστε ένα τυπικό υποσύστημα Java, για παράδειγμα ένα στοιχείο Swing, χρησιμοποιώντας το μοτίβο σχεδίασης Model-View-Controller (MVC). Τα αντικείμενα συμβάντων ενσωματώνουν ειδοποιήσεις αλλαγής από το μοντέλο. Οι προβολές καταγράφουν ενδιαφέρον για διάφορα συμβάντα προσθέτοντας ακροατές στο υποκείμενο μοντέλο του στοιχείου. Το μοντέλο ειδοποιεί τους θεατές του για αλλαγές στη δική του κατάσταση παραδίδοντας αυτά τα αντικείμενα συμβάντων στους εγγεγραμμένους ακροατές του. Συχνά, αυτοί οι ακροατές και οι τύποι συμβάντων είναι συγκεκριμένοι για τον τύπο μοντέλου και επομένως έχουν νόημα μόνο στο πλαίσιο του τύπου μοντέλου. Επειδή κάθε ένας από αυτούς τους τύπους ακροατών και συμβάντων πρέπει να είναι προσβάσιμος στο κοινό, ο καθένας πρέπει να βρίσκεται στο δικό του αρχείο προέλευσης. Σε αυτήν την περίπτωση, εκτός εάν χρησιμοποιείται κάποια σύμβαση κωδικοποίησης, η σύνδεση μεταξύ αυτών των τύπων είναι δύσκολο να αναγνωριστεί. Φυσικά, μπορεί κανείς να χρησιμοποιήσει ένα ξεχωριστό πακέτο για κάθε ομάδα για να δείξει τη σύζευξη, αλλά αυτό οδηγεί σε μεγάλο αριθμό πακέτων.

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

Πριν: Ένα παράδειγμα χωρίς ένθετα μαθήματα

Για παράδειγμα, αναπτύσσουμε ένα απλό συστατικό, Σχιστόλιθος, του οποίου η αποστολή είναι να σχεδιάζει σχήματα. Ακριβώς όπως τα εξαρτήματα Swing, χρησιμοποιούμε το σχέδιο σχεδίασης MVC. Το μοντέλο, Μοντέλο Slate, χρησιμεύει ως αποθετήριο για σχήματα. SlateModelListenerεγγραφείτε στις αλλαγές στο μοντέλο. Το μοντέλο ειδοποιεί τους ακροατές του στέλνοντας εκδηλώσεις τύπου SlateModelEvent. Σε αυτό το παράδειγμα, χρειαζόμαστε τρία αρχεία προέλευσης, ένα για κάθε τάξη:

// SlateModel.java εισαγωγή java.awt.Shape; δημόσια διεπαφή SlateModel {// Διαχείριση ακροατών δημόσια ακύρωση addSlateModelListener (SlateModelListener l); public void removeSlateModelListener (SlateModelListener l); // Διαχείριση αποθετηρίου σχήματος, οι προβολές χρειάζονται ειδοποίηση για το δημόσιο άκυρο addShape (Shape s); δημόσια ακύρωση removeShape (Shape s); public void removeAllShapes (); // Shape repository λειτουργίες μόνο για ανάγνωση δημόσια int getShapeCount (); δημόσιο Shape getShapeAtIndex (int index); } 
// SlateModelListener.java εισαγωγή java.util.EventListener; δημόσια διεπαφή SlateModelListener επεκτείνει το EventListener {public void slateChanged (εκδήλωση SlateModelEvent); } 
// SlateModelEvent.java εισαγωγή java.util.EventObject; Η δημόσια τάξη SlateModelEvent επεκτείνει το EventObject {public SlateModelEvent (μοντέλο SlateModel) {super (μοντέλο); }} 

(Ο πηγαίος κώδικας για DefaultSlateModel, η προεπιλεγμένη εφαρμογή για αυτό το μοντέλο, βρίσκεται στο αρχείο πριν / DefaultSlateModel.java.)

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

// Slate.java εισαγωγή javax.swing.JComponent; Δημόσια τάξη Slate επεκτείνει το JComponent υλοποιεί SlateModelListener {private SlateModel _model; δημόσια πλάκα (μοντέλο SlateModel) {_model = μοντέλο; _model.addSlateModelListener (αυτό); setOpaque (true); setUI (νέο SlateUI ()); } δημόσια πλάκα () {αυτό (νέο DefaultSlateModel ()); } δημόσιο SlateModel getModel () {return _model; } // Εφαρμογή ακρόασης public void slateChanged (SlateModelEvent event) {repaint (); }} 

Τελικά, SlateUI, το οπτικό στοιχείο GUI:

// SlateUI.java εισαγωγή java.awt. *; εισαγωγή javax.swing.JComponent; εισαγωγή javax.swing.plaf.ComponentUI; δημόσια τάξη Το SlateUI επεκτείνει το ComponentUI {public void paint (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; για (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}} 

Μετά: Ένα τροποποιημένο παράδειγμα με χρήση ένθετων τάξεων

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

Μπορούμε να βελτιώσουμε τα πράγματα κάνοντας SlateModelListener και SlateModelEvent ένθετοι τύποι του Μοντέλο Slate διεπαφή. Επειδή αυτοί οι ένθετοι τύποι βρίσκονται μέσα σε μια διεπαφή, είναι έμμεσα στατικοί. Ωστόσο, χρησιμοποιήσαμε μια ρητή στατική δήλωση για να βοηθήσουμε τον προγραμματιστή συντήρησης.

Ο κωδικός πελάτη θα τους αναφέρει ως SlateModel.SlateModelListener και SlateModel.SlateModelEvent, αλλά αυτό είναι περιττό και άσκοπα μακρύ. Καταργούμε το πρόθεμα Μοντέλο Slate από τις ένθετες τάξεις. Με αυτήν την αλλαγή, ο κωδικός πελάτη θα αναφέρεται σε αυτούς ως SlateModel.Listener και SlateModel.Event. Αυτό είναι σύντομο και σαφές και δεν εξαρτάται από τα πρότυπα κωδικοποίησης.

Για SlateUI, κάνουμε το ίδιο πράγμα - το κάνουμε μια ένθετη κατηγορία Σχιστόλιθος και αλλάξτε το όνομά του σε Διεπαφή χρήστη. Επειδή είναι μια ένθετη τάξη μέσα σε μια τάξη (και όχι μέσα σε μια διεπαφή), πρέπει να χρησιμοποιήσουμε έναν ρητό στατικό τροποποιητή.

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

// SlateModel.java εισαγωγή java.awt.Shape; εισαγωγή java.util.EventListener; εισαγωγή java.util.EventObject; δημόσια διεπαφή SlateModel {// Διαχείριση ακροατών δημόσια ακύρωση addSlateModelListener (SlateModel.Listener l); public void removeSlateModelListener (SlateModel.Listener l); // Διαχείριση αποθετηρίου σχήματος, οι προβολές χρειάζονται ειδοποίηση για το δημόσιο άκυρο addShape (Shape s); δημόσια ακύρωση removeShape (Shape s); public void removeAllShapes (); // Shape repository λειτουργίες μόνο για ανάγνωση δημόσια int getShapeCount (); δημόσιο Shape getShapeAtIndex (int index); // Σχετικές ένθετες τάξεις ανώτερου επιπέδου και διεπαφές δημόσια διεπαφή Το Listener επεκτείνει το EventListener {public void slateChanged (SlateModel.Event event); } Δημόσια τάξη Event επεκτείνει το EventObject {public Event (μοντέλο SlateModel) {super (μοντέλο). }}} 

Και ο κωδικός για Σχιστόλιθος αλλάζει σε:

// Slate.java εισαγωγή java.awt. *; εισαγωγή javax.swing.JComponent; εισαγωγή javax.swing.plaf.ComponentUI; Δημόσια τάξη Slate επεκτείνει το JComponent υλοποιεί SlateModel.Listener {public Slate (μοντέλο SlateModel) {_model = μοντέλο; _model.addSlateModelListener (αυτό); setOpaque (αληθινό); setUI (νέο Slate.UI ()); } δημόσια πλάκα () {αυτό (νέο DefaultSlateModel ()); } δημόσιο SlateModel getModel () {return _model; } // Εφαρμογή ακροατή δημόσιο άκυρο slateChanged (συμβάν SlateModel.Event) {repaint (); } το δημόσιο στατικό class UI επεκτείνει το ComponentUI {public void paint (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; για (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}} 

(Ο πηγαίος κώδικας για την προεπιλεγμένη εφαρμογή για το αλλαγμένο μοντέλο, DefaultSlateModel, βρίσκεται στο αρχείο μετά το / DefaultSlateModel.java.)

Μέσα στο Μοντέλο Slate κλάση, δεν είναι απαραίτητο να χρησιμοποιείτε πλήρως αναγνωρισμένα ονόματα για ένθετες τάξεις και διεπαφές. Για παράδειγμα, απλά Ακροατής θα αρκούσε στη θέση του SlateModel.Listener. Ωστόσο, η χρήση πλήρως αναγνωρισμένων ονομάτων βοηθά τους προγραμματιστές που αντιγράφουν υπογραφές μεθόδων από τη διεπαφή και τις επικολλούν σε κλάσεις υλοποίησης.

Το JFC και η χρήση ένθετων τάξεων

Η βιβλιοθήκη JFC χρησιμοποιεί ένθετες τάξεις σε ορισμένες περιπτώσεις. Για παράδειγμα, τάξη Βασικά σύνορα σε συσκευασία javax.swing.plaf.basic ορίζει πολλές ένθετες τάξεις όπως BasicBorders.ButtonBorder. Σε αυτήν την περίπτωση, τάξη Βασικά σύνορα δεν έχει άλλα μέλη και απλώς ενεργεί ως πακέτο. Αντίθετα, η χρήση ενός ξεχωριστού πακέτου θα ήταν εξίσου αποτελεσματική, αν όχι πιο κατάλληλη. Αυτή είναι διαφορετική χρήση από αυτήν που παρουσιάζεται σε αυτό το άρθρο.

Η χρήση αυτής της συμβουλής στην σχεδίαση JFC θα επηρέαζε την οργάνωση ακροατών και τύπων συμβάντων που σχετίζονται με τύπους μοντέλων. Για παράδειγμα, javax.swing.event.TableModelListener και javax.swing.event.TableModelEvent θα εφαρμοζόταν αντίστοιχα ως ένθετη διεπαφή και ένθετη τάξη μέσα javax.swing.table.TableModel.

Αυτή η αλλαγή, μαζί με τη συντόμευση των ονομάτων, θα έχει ως αποτέλεσμα μια διεπαφή ακροατή που ονομάζεται javax.swing.table.TableModel.Listener και μια τάξη εκδηλώσεων με το όνομα javax.swing.table.TableModel.Event. Μοντέλο πίνακα τότε θα ήταν πλήρως αυτόνομο με όλες τις απαραίτητες τάξεις υποστήριξης και διασυνδέσεις αντί να χρειαζόμαστε τάξεις υποστήριξης και διεπαφή απλωμένα σε τρία αρχεία και δύο πακέτα.

Οδηγίες για τη χρήση ένθετων τάξεων

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

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

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

  1. Είναι δυνατόν να ταξινομηθεί σαφώς μία από τις τάξεις ως η κύρια τάξη και η άλλη ως μια τάξη υποστήριξης;

  2. Η κατηγορία υποστήριξης δεν έχει νόημα εάν η κύρια κλάση αφαιρεθεί από το υποσύστημα;

συμπέρασμα

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

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

Ο Ramnivas Laddad είναι πιστοποιημένος από την Sun αρχιτέκτονας της τεχνολογίας Java (Java 2). Έχει μεταπτυχιακό στην ηλεκτρολογία με ειδίκευση στην επικοινωνία. Έχει έξι χρόνια εμπειρίας στο σχεδιασμό και την ανάπτυξη πολλών προγραμμάτων λογισμικού που περιλαμβάνουν GUI, δικτύωση και κατανεμημένα συστήματα. Έχει αναπτύξει αντικειμενοστρεφή συστήματα λογισμικού στην Java τα τελευταία δύο χρόνια και στο C ++ τα τελευταία πέντε χρόνια. Ο Ramnivas εργάζεται επί του παρόντος στη Real-Time Innovations Inc. ως μηχανικός λογισμικού. Στο RTI, αυτή τη στιγμή εργάζεται για να σχεδιάσει και να αναπτύξει το ControlShell, το πλαίσιο προγραμματισμού που βασίζεται σε στοιχεία για την κατασκευή σύνθετων συστημάτων πραγματικού χρόνου.