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

Οι παγίδες και οι βελτιώσεις του προτύπου αλυσίδας ευθύνης

Πρόσφατα έγραψα δύο προγράμματα Java (για λειτουργικό σύστημα Microsoft Windows) που πρέπει να εντοπίσουν συμβάντα καθολικού πληκτρολογίου που δημιουργούνται από άλλες εφαρμογές που εκτελούνται ταυτόχρονα στην ίδια επιφάνεια εργασίας. Η Microsoft παρέχει έναν τρόπο να το κάνει αυτό, εγγράφοντας τα προγράμματα ως παγκόσμιο ακροατήριο αγκίστρου πληκτρολογίου. Η κωδικοποίηση δεν χρειάστηκε πολύς χρόνος, αλλά ο εντοπισμός σφαλμάτων έκανε. Τα δύο προγράμματα φάνηκαν να λειτουργούν καλά όταν δοκιμάστηκαν ξεχωριστά, αλλά απέτυχαν όταν δοκιμάστηκαν μαζί. Περαιτέρω δοκιμές αποκάλυψαν ότι όταν τα δύο προγράμματα έτρεχαν μαζί, το πρόγραμμα που ξεκίνησε πρώτο δεν ήταν πάντα σε θέση να πιάσει τα παγκόσμια βασικά γεγονότα, αλλά η εφαρμογή που ξεκίνησε αργότερα λειτούργησε καλά.

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

Το μυστήριο λύθηκε, αλλά ήμουν δυσαρεστημένος με το πλαίσιο αγκίστρου. Πρώτον, με απαιτεί να "θυμάμαι" για να εισαγάγω το CallNextHookEx () μέθοδος κλήση στον κωδικό μου. Δεύτερον, το πρόγραμμά μου θα μπορούσε να απενεργοποιήσει άλλα προγράμματα και αντίστροφα. Γιατί συμβαίνει αυτό; Επειδή η Microsoft εφάρμοσε το παγκόσμιο πλαίσιο αγκίστρου ακολουθώντας ακριβώς το κλασικό μοτίβο Chain of Responsibility (CoR) που ορίστηκε από το Gang of Four (GoF).

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

Κλασική ΕτΠ

Το κλασικό μοτίβο CoR που ορίζεται από το GoF in Σχεδιαστικά πρότυπα:

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

Το σχήμα 1 απεικονίζει το διάγραμμα τάξης.

Μια τυπική δομή αντικειμένων μπορεί να μοιάζει με το Σχήμα 2.

Από τις παραπάνω εικόνες, μπορούμε να συνοψίσουμε ότι:

  • Πολλοί χειριστές μπορεί να είναι σε θέση να χειριστούν ένα αίτημα
  • Μόνο ένας χειριστής χειρίζεται το αίτημα
  • Ο αιτών γνωρίζει μόνο μια αναφορά σε έναν χειριστή
  • Ο αιτών δεν γνωρίζει πόσους χειριστές είναι σε θέση να χειριστεί το αίτημά του
  • Ο αιτών δεν γνωρίζει ποιος χειριστής χειρίστηκε το αίτημά του
  • Ο αιτών δεν έχει κανένα έλεγχο των χειριστών
  • Οι χειριστές θα μπορούσαν να καθοριστούν δυναμικά
  • Η αλλαγή της λίστας χειριστών δεν θα επηρεάσει τον κωδικό του αιτούντος

Τα τμήματα κώδικα παρακάτω δείχνουν τη διαφορά μεταξύ του κώδικα αιτούντος που χρησιμοποιεί CoR και του κώδικα αιτούντος που δεν το κάνει.

Κωδικός αιτούντος που δεν χρησιμοποιεί CoR:

 χειριστές = getHandlers (); για (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (request); εάν (χειριστές [i] .handled ()) break; } 

Κωδικός αιτούντος που χρησιμοποιεί CoR:

 getChain (). λαβή (αίτημα); 

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

 Διαχειριστής δημόσιας τάξης {ιδιωτικός διάδοχος χειριστή δημόσιο χειριστή (HelpHandler s) {διαδόχος = s; } δημόσια λαβή (αίτημα ARequest) {if (διαδόχος! = null) διαδόχος.handle (αίτημα); }} Η δημόσια τάξη AHandler επεκτείνει το Handler {public handle (ARequest request) {if (someCondition) // Χειρισμός: κάντε κάτι άλλο super.handle (request); }} 

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

Κενό του παγκόσμιου πλαισίου αγκίστρου των Microsoft Windows και του πλαισίου φίλτρου servlet Java

Η εφαρμογή του παγκόσμιου πλαισίου αγκίστρου των Microsoft Windows είναι η ίδια με την κλασική εφαρμογή CoR που προτείνει η GoF. Το πλαίσιο εξαρτάται από τους μεμονωμένους ακροατές γάντζου για να φτιάξουν το CallNextHookEx () καλέστε και μεταδώστε το συμβάν μέσω της αλυσίδας. Υποθέτει ότι οι προγραμματιστές θα θυμούνται πάντα τον κανόνα και δεν θα ξεχάσουν ποτέ να κάνουν την κλήση. Φυσικά, μια παγκόσμια αλυσίδα γάντζων εκδηλώσεων δεν είναι κλασική CoR. Η εκδήλωση πρέπει να παραδοθεί σε όλους τους ακροατές της αλυσίδας, ανεξάρτητα από το αν ο ακροατής το χειρίζεται ήδη. Ετσι το CallNextHookEx () η κλήση φαίνεται να είναι δουλειά της βασικής τάξης, όχι των μεμονωμένων ακροατών. Το να αφήσετε τους μεμονωμένους ακροατές να κάνουν την κλήση δεν κάνει κανένα καλό και εισάγει τη δυνατότητα διακοπής της αλυσίδας κατά λάθος.

Το πλαίσιο φίλτρου servlet Java κάνει παρόμοιο λάθος με το παγκόσμιο άγκιστρο των Microsoft Windows. Ακολουθεί ακριβώς την εφαρμογή που προτείνει η GoF. Κάθε φίλτρο αποφασίζει εάν θα κυλήσει ή θα σταματήσει την αλυσίδα τηλεφωνώντας ή όχι doFilter () στο επόμενο φίλτρο. Ο κανόνας εφαρμόζεται μέσω javax.servlet.Filter # doFilter () τεκμηρίωση:

"4. α) Καλέστε την επόμενη οντότητα στην αλυσίδα χρησιμοποιώντας το Αλυσίδα φίλτρου αντικείμενο (chain.doFilter ()), 4. β) ή να μην μεταβιβάσει το ζεύγος αιτήσεων / απόκρισης στην επόμενη οντότητα στην αλυσίδα φίλτρου για να αποκλείσει την επεξεργασία του αιτήματος.

Εάν ένα φίλτρο ξεχάσει να κάνει το chain.doFilter () κλήση όταν πρέπει να έχει, θα απενεργοποιήσει άλλα φίλτρα στην αλυσίδα. Εάν ένα φίλτρο κάνει το chain.doFilter () καλέστε όταν πρέπει δεν έχει, θα επικαλεστεί άλλα φίλτρα στην αλυσίδα.

Λύση

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

Classic CoR: Αποστολή αιτήματος μέσω της αλυσίδας έως ότου ένας κόμβος χειριστεί το αίτημα

Αυτή είναι η εφαρμογή που προτείνω για την κλασική ΕτΠ:

 / ** * Classic CoR, δηλαδή, το αίτημα αντιμετωπίζεται μόνο από έναν από τους χειριστές της αλυσίδας. * / δημόσια αφηρημένη κλάση ClassicChain {/ ** * Ο επόμενος κόμβος στην αλυσίδα. * / ιδιωτικό ClassicChain στη συνέχεια. δημόσιο ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Αφετηρία της αλυσίδας, που καλείται από τον πελάτη ή τον προ-κόμβο. * Λαβή κλήσης () σε αυτόν τον κόμβο και αποφασίστε εάν θα συνεχίσετε την αλυσίδα. Εάν ο επόμενος κόμβος δεν είναι null και * αυτός ο κόμβος δεν χειρίστηκε το αίτημα, ξεκινήστε κλήση () στον επόμενο κόμβο για να διαχειριστείτε το αίτημα. * @param request the request parameter * / public final void start (ARequest request) {boolean handledByThisNode = this.handle (request); if (next! = null &&! handledByThisNode) next.start (αίτημα); } / ** * Κλήθηκε από την αρχή (). * @param request the request parameter * @return a boolean υποδεικνύει εάν αυτός ο κόμβος χειρίστηκε το αίτημα * / προστατευμένη αφηρημένη boolean λαβή (ARequest request). } δημόσια τάξη AClassicChain επεκτείνει το ClassicChain {/ ** * Κλήθηκε από την αρχή (). * @param request the request parameter * @return a boolean υποδεικνύει εάν αυτός ο κόμβος χειρίστηκε το αίτημα * / προστατευμένη boolean λαβή (ARequest request) {boolean handledByThisNode = false; if (someCondition) {// Να χειρίζεται handledByThisNode = true; } επιστροφή handledByThisNode; }} 

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

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

Μη κλασικό CoR 1: Στείλτε αίτημα μέσω της αλυσίδας έως ότου ένας κόμβος θέλει να σταματήσει

Αυτός ο τύπος εφαρμογής της ΕτΠ είναι μια μικρή παραλλαγή του κλασικού μοτίβου της ΕτΠ. Η αλυσίδα σταματά όχι επειδή ένας κόμβος έχει χειριστεί το αίτημα, αλλά επειδή ένας κόμβος θέλει να σταματήσει. Σε αυτήν την περίπτωση, η κλασική εφαρμογή της ΕτΠ ισχύει επίσης εδώ, με μια μικρή εννοιολογική αλλαγή: η σημαία Boolean επέστρεψε από το λαβή() Η μέθοδος δεν δείχνει εάν το αίτημα έχει αντιμετωπιστεί. Αντίθετα, λέει στη βασική τάξη εάν πρέπει να σταματήσει η αλυσίδα. Το πλαίσιο φίλτρου servlet ταιριάζει σε αυτήν την κατηγορία. Αντί να αναγκάζετε μεμονωμένα φίλτρα να καλούν chain.doFilter (), η νέα εφαρμογή αναγκάζει το μεμονωμένο φίλτρο να επιστρέψει ένα Boolean, το οποίο είναι συμβατό από τη διεπαφή, κάτι που ο προγραμματιστής δεν ξεχνά ποτέ ούτε χάνει.

Μη κλασική ΕτΠ 2: Ανεξάρτητα από το χειρισμό αιτημάτων, στείλτε αίτημα σε όλους τους χειριστές

Για αυτόν τον τύπο εφαρμογής της ΕτΠ, λαβή() δεν χρειάζεται να επιστρέψει την ένδειξη Boolean, επειδή το αίτημα αποστέλλεται σε όλους τους χειριστές ανεξάρτητα. Αυτή η εφαρμογή είναι ευκολότερη. Επειδή από τη φύση του το Microsoft global hook hook από τη φύση του ανήκει σε αυτόν τον τύπο CoR, η ακόλουθη εφαρμογή θα πρέπει να διορθώσει το κενό της:

 / ** * Non-Classic CoR 2, δηλαδή, το αίτημα αποστέλλεται σε όλους τους χειριστές ανεξάρτητα από το χειρισμό. * / δημόσια αφηρημένη κλάση NonClassicChain2 {/ ** * Ο επόμενος κόμβος στην αλυσίδα. * / ιδιωτικό NonClassicChain2 επόμενο. δημόσιο NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Αφετηρία της αλυσίδας, που καλείται από τον πελάτη ή τον προ-κόμβο. * Κλήση κλήσης () σε αυτόν τον κόμβο και μετά έναρξη κλήσης () στον επόμενο κόμβο εάν υπάρχει επόμενος κόμβος. * @param request the request parameter * / public final void start (ARequest request) {this.handle (request); if (next! = null) next.start (αίτημα); } / ** * Κλήθηκε από την αρχή (). * @param request η παράμετρος αίτησης * / προστατευμένη λαβή κενού abstract (ARequest request) } δημόσια τάξη ANonClassicChain2 επεκτείνει το NonClassicChain2 {/ ** * Κλήθηκε από την αρχή (). * @param request the request parameter * / protected void handle (ARequest request) {// Κάντε χειρισμό. }} 

Παραδείγματα

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

Παράδειγμα 1