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

Περισσότερα για τους λάτρεις και τους ρυθμιστές

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

Δυστυχώς, το ιδίωμα getter / setter που πολλοί προγραμματιστές θεωρούν ως αντικειμενικό προσβάλλει αυτή τη θεμελιώδη αρχή OO στα μπαστούνια. Εξετάστε το παράδειγμα του a Χρήματα τάξη που έχει getValue () μέθοδο που επιστρέφει την "αξία" σε δολάρια. Θα έχετε κωδικό όπως το παρακάτω σε όλο το πρόγραμμά σας:

διπλή παραγγελία Σύνολο; Ποσό χρημάτων = ...; //... orderTotal + = ποσό.getValue (); // παραγγελία Το σύνολο πρέπει να είναι σε δολάρια

Το πρόβλημα με αυτήν την προσέγγιση είναι ότι ο παραπάνω κώδικας κάνει μια μεγάλη υπόθεση για το πώς το Χρήματα εφαρμόζεται κλάση (ότι η "τιμή" αποθηκεύεται σε ένα διπλό). Κωδικός που κάνει τις παραδοχές υλοποίησης να διακόπτονται όταν αλλάζει η εφαρμογή. Εάν, για παράδειγμα, πρέπει να διεθνοποιήσετε την αίτησή σας για να υποστηρίξετε νομίσματα εκτός από δολάρια, τότε getValue () δεν επιστρέφει τίποτα σημαντικό. Θα μπορούσατε να προσθέσετε ένα getCurrency (), αλλά αυτό θα έκανε όλο τον κώδικα που περιβάλλει το getValue () καλέστε πολύ πιο περίπλοκο, ειδικά εάν συνεχίζετε να χρησιμοποιείτε τη στρατηγική getter / setter για να λάβετε τις πληροφορίες που χρειάζεστε για να κάνετε τη δουλειά. Μια τυπική (ελαττωματική) εφαρμογή μπορεί να μοιάζει με αυτήν:

Ποσό χρημάτων = ...; //... τιμή = ποσό.getValue (); currency = ποσό.getCurrency (); μετατροπή = CurrencyTable.getConversionFactor (νόμισμα, USDOLLARS); συνολική + = τιμή * μετατροπή; //...

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

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

Σύνολο χρημάτων = ...; Ποσό χρημάτων = ...; total.increaseBy (ποσό); 

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

Το πρόβλημα

Οι περισσότεροι προγραμματιστές δεν έχουν καμία δυσκολία να κατανοήσουν αυτήν την ιδέα σε επίπεδο επιχειρησιακής λογικής (αν και μπορεί να χρειαστεί λίγη προσπάθεια να σκεφτούμε με συνέπεια αυτόν τον τρόπο). Ωστόσο, τα προβλήματα αρχίζουν να εμφανίζονται όταν το περιβάλλον εργασίας χρήστη (UI) εισάγει την εικόνα. Το πρόβλημα δεν είναι ότι δεν μπορείτε να εφαρμόσετε τεχνικές όπως αυτή που μόλις περιέγραψα για τη δημιουργία ενός περιβάλλοντος εργασίας χρήστη, αλλά ότι πολλοί προγραμματιστές είναι κλειδωμένοι σε μια νοοτροπία λήψης / ρύθμισης όταν πρόκειται για διεπαφές χρήστη. Κατηγορώ αυτό το πρόβλημα σε βασικά διαδικαστικά εργαλεία κατασκευής κώδικα όπως η Visual Basic και οι κλώνοι της (συμπεριλαμβανομένων των Java UI builders) που σας αναγκάζουν σε αυτόν τον διαδικαστικό τρόπο, τρόπους συλλογής.

(Digression: Μερικοί από εσάς θα αποκλείσετε την προηγούμενη δήλωση και θα φωνάξετε ότι το VB βασίζεται στην ιερή αρχιτεκτονική Model-View-Controller (MVC), όπως και ιερό. Λάβετε υπόψη ότι το MVC αναπτύχθηκε πριν από σχεδόν 30 χρόνια. Τη δεκαετία του 1970, ο μεγαλύτερος υπερυπολογιστής ήταν ισοδύναμος με τους σημερινούς επιτραπέζιους υπολογιστές. Τα περισσότερα μηχανήματα (όπως το DEC PDP-11) ήταν υπολογιστές 16-bit, με μνήμη 64 KB και ταχύτητες ρολογιού σε δεκάδες megahertz. Η διεπαφή χρήστη σας ήταν πιθανώς στοίβα καρτών με διάτρηση. Αν ήσασταν αρκετά τυχεροί που είχατε ένα τερματικό βίντεο, τότε μπορεί να χρησιμοποιείτε ένα σύστημα εισόδου / εξόδου (I / O) με βάση την ASCII. Έχουμε μάθει πολλά τα τελευταία 30 χρόνια. Το Java Swing έπρεπε να αντικαταστήσει το MVC με παρόμοια αρχιτεκτονική "διαχωρίσιμου μοντέλου", κυρίως επειδή το καθαρό MVC δεν απομονώνει επαρκώς τα επίπεδα διεπαφής χρήστη και μοντέλου τομέα.)

Λοιπόν, ας ορίσουμε με λίγα λόγια το πρόβλημα:

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

Σημειώστε, παρεμπιπτόντως, ότι δεν κρύβετε το γεγονός ότι υπάρχει ένα χαρακτηριστικό. (Ορίζω Χαρακτηριστικό, εδώ, ως βασικό χαρακτηριστικό του αντικειμένου.) Ξέρετε ότι ένα Υπάλληλος πρέπει να έχει χαρακτηριστικό μισθού ή μισθού, αλλιώς δεν θα ήταν Υπάλληλος. (Θα ήταν ένα Πρόσωπο, ένα Εθελοντής, ένα Περιπλανώμενος, ή κάτι άλλο που δεν έχει μισθό.) Αυτό που δεν γνωρίζετε - ή θέλετε να ξέρετε - είναι πώς αυτός ο μισθός αντιπροσωπεύεται μέσα στο αντικείμενο. Θα μπορούσε να είναι διπλό, ένα Σειρά, μια κλίμακα μακρύς, ή δυαδικό κωδικοποιημένο δεκαδικό. Μπορεί να είναι ένα "συνθετικό" ή "παράγωγο" χαρακτηριστικό, το οποίο υπολογίζεται κατά το χρόνο εκτέλεσης (για παράδειγμα από έναν βαθμό αμοιβής ή έναν τίτλο εργασίας ή από την ανάκτηση της τιμής από μια βάση δεδομένων). Αν και μια μέθοδος get μπορεί πράγματι να κρύψει μερικές από αυτές τις λεπτομέρειες εφαρμογής, όπως είδαμε με το Χρήματα Για παράδειγμα, δεν μπορεί να κρύψει αρκετά.

Πώς λοιπόν ένα αντικείμενο παράγει το δικό του περιβάλλον εργασίας χρήστη και παραμένει διατηρήσιμο; Μόνο τα πιο απλοϊκά αντικείμενα μπορούν να υποστηρίξουν κάτι σαν displayYself () μέθοδος. Τα ρεαλιστικά αντικείμενα πρέπει:

  • Εμφανίζονται σε διαφορετικές μορφές (XML, SQL, τιμές διαχωρισμένες με κόμμα, κ.λπ.).
  • Εμφάνιση διαφορετικών προβολές του εαυτού τους (μία προβολή μπορεί να εμφανίζει όλα τα χαρακτηριστικά · μια άλλη μπορεί να εμφανίζει μόνο ένα υποσύνολο των χαρακτηριστικών και μια τρίτη μπορεί να παρουσιάζει τα χαρακτηριστικά με διαφορετικό τρόπο).
  • Εμφανίζονται σε διαφορετικά περιβάλλοντα (από την πλευρά του πελάτη (JComponent) και σερβίρεται σε πελάτη (HTML), για παράδειγμα) και χειρίζεται τόσο την είσοδο όσο και την έξοδο και στα δύο περιβάλλοντα.

Μερικοί από τους αναγνώστες του προηγούμενου άρθρου μου για το getter / setter έφτασαν στο συμπέρασμα ότι υποστηρίζω ότι προσθέτετε μεθόδους στο αντικείμενο για να καλύψετε όλες αυτές τις δυνατότητες, αλλά αυτή η "λύση" είναι προφανώς παράλογη. Όχι μόνο το προκύπτον αντικείμενο βαρέων βαρών είναι πολύ περίπλοκο, θα πρέπει να το τροποποιείτε συνεχώς για να χειρίζεστε νέες απαιτήσεις διεπαφής χρήστη. Πρακτικά, ένα αντικείμενο δεν μπορεί να δημιουργήσει όλες τις πιθανές διεπαφές χρήστη για τον εαυτό του, εάν για κανέναν άλλο λόγο από ότι πολλά από αυτά τα περιβάλλοντα χρήστη δεν είχαν καν συλληφθεί κατά τη δημιουργία της τάξης.

Δημιουργήστε μια λύση

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

Αυτή η διακλάδωση των μεθόδων ενός αντικειμένου εμφανίζεται σε διάφορα σχέδια σχεδίασης. Πιθανότατα γνωρίζετε τη στρατηγική, η οποία χρησιμοποιείται με τα διάφορα java.awt. Δοχείο τάξεις για τη διάταξη. Θα μπορούσατε να λύσετε το πρόβλημα διάταξης με μια λύση παραγώγων: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, κ.λπ., αλλά αυτό επιβάλλει πάρα πολλές κλάσεις και πολύ διπλό κώδικα σε αυτές τις κλάσεις. Μία λύση βαρέων βαρών (προσθήκη μεθόδων σε Δοχείο σαν layOutAsGrid (), layOutAsFlow (), κ.λπ.) είναι επίσης ανέφικτο επειδή δεν μπορείτε να τροποποιήσετε τον πηγαίο κώδικα για το Δοχείο απλώς και μόνο επειδή χρειάζεστε μια μη υποστηριζόμενη διάταξη. Στο σχέδιο στρατηγικής, δημιουργείτε ένα Στρατηγική διεπαφή (LayoutManager) υλοποιήθηκε από πολλούς Συγκεκριμένη στρατηγική τάξεις (FlowLayout, GridLayout, και τα λοιπά.). Στη συνέχεια πείτε ένα Συμφραζόμενα αντικείμενο (α Δοχείο) πώς να κάνετε κάτι περνώντας το α Στρατηγική αντικείμενο. (Περνάτε ένα Δοχείο ένα LayoutManager που καθορίζει μια στρατηγική διάταξης.)

Το μοτίβο Builder είναι παρόμοιο με τη στρατηγική. Η κύρια διαφορά είναι ότι το Οικοδόμος η τάξη εφαρμόζει μια στρατηγική για την κατασκευή κάτι (όπως ένα JComponent ή ροή XML που αντιπροσωπεύει την κατάσταση ενός αντικειμένου). Οικοδόμος τα αντικείμενα συνήθως κατασκευάζουν τα προϊόντα τους χρησιμοποιώντας μια διαδικασία πολλών σταδίων. Δηλαδή, κλήσεις σε διάφορες μεθόδους του Οικοδόμος απαιτούνται για την ολοκλήρωση της διαδικασίας κατασκευής, και το Οικοδόμος Συνήθως δεν γνωρίζει τη σειρά με την οποία θα πραγματοποιούνται οι κλήσεις ή τον αριθμό των φορών που θα καλείται μία από τις μεθόδους του. Το πιο σημαντικό χαρακτηριστικό του οικοδόμου είναι ότι το επιχειρηματικό αντικείμενο (ονομάζεται Συμφραζόμενα) δεν ξέρει ακριβώς τι Οικοδόμος το αντικείμενο χτίζει. Το μοτίβο απομονώνει το επιχειρηματικό αντικείμενο από την αναπαράστασή του.

Ο καλύτερος τρόπος για να δείτε πώς λειτουργεί ένα απλό οικοδόμο είναι να κοιτάξετε ένα. Πρώτα ας δούμε το Συμφραζόμενα, το επιχειρηματικό αντικείμενο που πρέπει να εκθέσει μια διεπαφή χρήστη. Η λίστα 1 δείχνει μια απλοϊκή Υπάλληλος τάξη. ο Υπάλληλος έχει όνομα, ταυτότητα, και Μισθός γνωρίσματα. (Τα stubs για αυτά τα μαθήματα βρίσκονται στο κάτω μέρος της λίστας, αλλά αυτά τα stubs είναι απλά placeholder για το πραγματικό πράγμα. Μπορείτε - ελπίζω - να φανταστείτε εύκολα πώς θα λειτουργούσαν αυτά τα μαθήματα.)

Αυτό το συγκεκριμένο Συμφραζόμενα χρησιμοποιεί αυτό που πιστεύω ως αμφίδρομο εργαλείο δημιουργίας. Το κλασικό Gang of Four Builder πηγαίνει προς μία κατεύθυνση (έξοδος), αλλά έχω προσθέσει επίσης ένα Οικοδόμος ότι ένα Υπάλληλος αντικείμενο μπορεί να χρησιμοποιήσει για να αρχικοποιηθεί. Δύο Οικοδόμος απαιτούνται διεπαφές. ο Υπάλληλος. Εξαγωγέας διεπαφή (Λίστα 1, γραμμή 8) χειρίζεται την κατεύθυνση εξόδου. Ορίζει μια διεπαφή σε ένα Οικοδόμος αντικείμενο που κατασκευάζει την αναπαράσταση του τρέχοντος αντικειμένου. ο Υπάλληλος αναθέτει την πραγματική κατασκευή UI στο Οικοδόμος στο εξαγωγή() μέθοδος (στη γραμμή 31). ο Οικοδόμος δεν περνά τα πραγματικά πεδία, αλλά χρησιμοποιεί Σειράνα περάσει μια αναπαράσταση αυτών των πεδίων.

Λίστα 1. Υπάλληλος: Το πλαίσιο οικοδόμου

 1 εισαγωγή java.util.Locale; 2 3 δημόσια τάξη Υπάλληλος 4 {ιδιωτικό όνομα Όνομα; 5 ιδιωτική ταυτότητα EmployeeId; 6 ιδιωτικός μισθός χρημάτων 7 8 δημόσια διεπαφή Εξαγωγέας 9 {void addName (String name); 10 void addID (String id). 11 άκυρο addSalary (String μισθός)? 12} 13 14 δημόσια διεπαφή Εισαγωγέας 15 {String παρέχουνName (); 16 string stringID (); 17 συμβολοσειρά παρέχει μισθός (); 18 άκυρο ανοιχτό (); 19 άκυρο κλείσιμο (); 20} 21 22 δημόσιος υπάλληλος (Εισαγωγέας) 23 {builder.open (); 24 this.name = νέο όνομα (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = νέα χρήματα (builder.provideSalary (), 27 νέα τοπικά ("en", "US")); 28 builder.close (); 29} 30 31 δημόσια ακύρωση εξαγωγής (Exporter builder) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (gaji.toString ()); 35} 36 37 //... 38 } 39 //---------------------------------------------------------------------- 40 // Μονάδα δοκιμής μονάδας 41 // 42 κλάση Όνομα 43 {ιδιωτική τιμή συμβολοσειράς; 44 δημόσιο όνομα (τιμή συμβολοσειράς) 45 {this.value = value; 46} 47 δημόσια συμβολοσειρά toString () {τιμή επιστροφής; }; 48} 49 50 τάξη EmployeeId 51 {ιδιωτική τιμή συμβολοσειράς; 52 δημόσιο EmployeeId (String value) 53 {this.value = value; 54} 55 δημόσια συμβολοσειρά toString () {τιμή επιστροφής; } 56} 57 58 κατηγορίας Money 59 {private String value; 60 δημόσια χρήματα (Τιμή συμβολοσειράς, τοπική τοποθεσία) 61 {this.value = value; 62} 63 δημόσια συμβολοσειρά toString () {τιμή επιστροφής; } 64} 

Ας δούμε ένα παράδειγμα. Ο παρακάτω κώδικας δημιουργεί τη διεπαφή χρήστη του σχήματος 1:

Υπάλληλος wilma = ...; JComponentExporter uiBuilder = νέο JComponentExporter (); // Δημιουργήστε το πρόγραμμα δημιουργίας wilma.export (uiBuilder); // Δημιουργήστε το περιβάλλον εργασίας χρήστη JComponent userInterface = uiBuilder.getJComponent (); //... someContainer.add (userInterface); 

Η λίστα 2 δείχνει την πηγή για το JComponentExporter. Όπως μπορείτε να δείτε, όλοι οι κωδικοί που σχετίζονται με το περιβάλλον χρήστη συγκεντρώνονται στο Οικοδόμος σκυροδέματος (ο JComponentExporter), και το Συμφραζόμενα (ο Υπάλληλος) οδηγεί τη διαδικασία κατασκευής χωρίς να γνωρίζει ακριβώς τι δημιουργεί.

Λίστα 2. Εξαγωγή σε περιβάλλον εργασίας πελάτη

 1 εισαγωγή javax.swing. *; 2 εισαγωγή java.awt. *; 3 εισαγωγή java.awt.event. *; 4 5 class JComponentExporter εφαρμόζει Employee.Exporter 6 {private String name, id, gaji; 7 8 public void addName (όνομα συμβολοσειράς) {this.name = name; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (String gaji) {this.salary = μισθός; } 11 12 JComponent getJComponent () 13 {JComponent panel = νέο JPanel (); 14 panel.setLayout (νέο GridLayout (3,2)); 15 panel.add (νέα ετικέτα JL ("Όνομα:")); 16 panel.add (νέα ετικέτα JL (όνομα)); 17 panel.add (νέο JLabel ("Αναγνωριστικό υπαλλήλου:")); 18 panel.add (νέα JLabel (id)); 19 panel.add (νέο JLabel ("Μισθός:")); 20 panel.add (νέα ετικέτα JL (μισθός)) 21 πάνελ επιστροφής; 22} 23}