Ρόλοι και ανάθεση#

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

Αυτό το κεφάλαιο καλύπτει τα τρία πρότυπα σύνθεσης που χρησιμοποιεί στην πράξη ο κώδικας της pperl:

  1. Κοινή χρήση τύπου mixin μέσω ενός μικροσκοπικού βοηθητικού πακέτου.

  2. Σύνθεση ρόλων μέσω Role::Tiny ή ενός συστήματος της οικογένειας Moose σε παλαιό κώδικα.

  3. Ανάθεση - προώθηση μεθόδων σε ένα εμπεριεχόμενο αντικείμενο.

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

Το πρόβλημα#

Φανταστείτε μια κλάση Radio και μια κλάση Computer. Και οι δύο έχουν διακόπτη on/off. Δεν σχετίζονται - ένα ραδιόφωνο δεν είναι υπολογιστής, και κανένα από τα δύο δεν εξειδικεύει μια υποθετική Machine. Μια γονική κλάση HasOnOffSwitch θα ήταν τεχνητή ομαδοποίηση που μοναδικός σκοπός της είναι η επαναχρησιμοποίηση δύο μεθόδων.

Τρεις λύσεις, κατά αύξουσα σειρά φιλοδοξίας:

  • Αντιγράψτε τις μεθόδους turn_on / turn_off σε κάθε κλάση. Ανεκτό για δύο κλάσεις, οδυνηρό για δέκα.

  • Εξάγετέ τις σε ένα βοηθητικό πακέτο, χρησιμοποιήστε έναν από τους μηχανισμούς κοινής χρήσης παρακάτω.

  • Χρησιμοποιήστε ένα σύστημα ρόλων με τυπική σημασιολογία σύνθεσης.

Πρότυπο 1: χειροκίνητο mixin#

Ο μικρότερος δυνατός μηχανισμός «μοιράσου αυτές τις μεθόδους» είναι ένα απλό πακέτο που εγκαθιστά υπορουτίνες στην κλάση-καταναλωτή κατά τον χρόνο use:

package HasOnOffSwitch;

sub import {
    my $target = caller;
    no strict 'refs';
    *{"${target}::turn_on"}  = sub { $_[0]->{is_on} = 1 };
    *{"${target}::turn_off"} = sub { $_[0]->{is_on} = 0 };
    *{"${target}::is_on"}    = sub { $_[0]->{is_on} };
}

1;

Σε χρήση:

package Radio;
use HasOnOffSwitch;

sub new { bless { is_on => 0 }, shift }

Αυτό λειτουργεί. Είναι επίσης εύθραυστο: φτάνει απευθείας στο hash της κλάσης-καταναλωτή ($_[0]->{is_on}), που είναι ακριβώς η παραβίαση ενθυλάκωσης για την οποία φημίζεται η κλασική αντικειμενοστρέφεια με υπόβαθρο hash. Σπάει αν ο καταναλωτής είναι κλάση δηλωμένη με class, επειδή τα πεδία δεν είναι hash-slot.

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

Πρότυπο 2: Role::Tiny#

Το Role::Tiny είναι το ελαφρύ σύστημα ρόλων που λειτουργεί με απλές κλασικές κλάσεις, με Moo και με Moose (στην πλευρά του καταναλωτή). Είναι ό,τι πιο κοντινό σε φορητή πρωτογενή οντότητα ρόλου στο οικοσύστημα του CPAN.

package HasOnOffSwitch;
use Role::Tiny;

sub turn_on  { $_[0]->is_on(1) }
sub turn_off { $_[0]->is_on(0) }

requires 'is_on';       # the consumer must provide this accessor

1;

Ο καταναλωτής υιοθετεί τον ρόλο με with:

package Radio;
use Role::Tiny::With;
with 'HasOnOffSwitch';

sub new    { bless { is_on => 0 }, shift }
sub is_on  { @_ > 1 ? $_[0]{is_on} = $_[1] : $_[0]{is_on} }

Τι σας δίνει το Role::Tiny σε σχέση με το χειροκίνητο mixin:

  • Απαιτήσεις. Το requires 'is_on' αποτυγχάνει κατά τον χρόνο σύνθεσης αν ο καταναλωτής δεν παρέχει is_on. Το χειροκίνητο mixin αποτυγχάνει σιωπηρά στην πρώτη κλήση μεθόδου.

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

  • Method modifiers - before, after, around - αν τους θέλετε.

Το Role::Tiny δεν είναι το ίδιο πράγμα με το σύστημα ρόλων του Moose. Η εμβέλειά του είναι σκόπιμα στενή. Αν η βάση κώδικα χρησιμοποιεί ήδη Moose, χρησιμοποιήστε Moose::Role· αν χρησιμοποιεί Moo, χρησιμοποιήστε Moo::Role.

Πρότυπο 3: ρόλοι οικογένειας Moose#

Οι παλαιές βάσεις κώδικα χτισμένες σε Moose ή Moo χρησιμοποιούν Moose::Role και Moo::Role αντίστοιχα. Η επιφάνεια μοιάζει με το σύστημα ρόλων που θα αποκτήσει τελικά το χαρακτηριστικό class του πυρήνα:

package HasOnOffSwitch;
use Moose::Role;

has is_on => (is => 'rw', isa => 'Bool', default => 0);

sub turn_on  { $_[0]->is_on(1) }
sub turn_off { $_[0]->is_on(0) }

1;

package Radio;
use Moose;
with 'HasOnOffSwitch';

1;

Στην pperl, το Moose εξακολουθεί να λειτουργεί. Ο νέος κώδικας δεν θα έπρεπε να στρέφεται σε αυτό - το κόστος στον χρόνο εκτέλεσης και το βάρος των εξαρτήσεων είναι αμφότερα μεγάλα, και το χαρακτηριστικό class του πυρήνα καλύπτει πλέον τη δηλωτική πλευρά για την οποία χτίστηκε το Moose. Όταν φτάσει το χαρακτηριστικό ρόλων του πυρήνα, η μετάβαση από Moose::Role στον ρόλο του πυρήνα θα είναι το φυσικό επόμενο βήμα.

Πρότυπο 4: ανάθεση#

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

Η χειροκίνητη μορφή:

use feature 'class';

class Player {
    field $radio :param;

    method turn_on  { $radio->turn_on }
    method turn_off { $radio->turn_off }
    method is_on    { $radio->is_on }
}

Κάθε προωθούμενη μέθοδος είναι τρεις γραμμές μηχανικής σωλήνωσης. Για δύο ή τρεις μεθόδους αυτό είναι εντάξει· για ευρεία επιφάνεια, στραφείτε στο Class::Method::Delegate ή σε accessor handles του Moose:

package Player;
use Moose;

has radio => (
    is      => 'ro',
    handles => ['turn_on', 'turn_off', 'is_on'],
);

Το handles δημιουργεί τους προωθητές κατά τον χρόνο κατασκευής της κλάσης.

Πότε να αναθέτετε έναντι να κληρονομείτε#

  • Αναθέστε όταν το εμπεριεχόμενο αντικείμενο είναι συνεργάτης - κάτι που ο κάτοχος έχει αντί να είναι. Ένας Player έχει ένα Radio· ένας Player δεν είναι είδος Radio.

  • Κληρονομήστε όταν το παιδί γνήσια είναι ένας εξειδικευμένος γονέας. Ένας Circle είναι είδος Shape· ένα File::MP3 είναι είδος File.

Αν η απάντηση στο «is-a ή has-a;» δεν είναι άμεση, η σχέση είναι πιθανότατα has-a. Η προεπιλογή της σύνθεσης είναι σχεδόν πάντα η λιγότερο λανθασμένη επιλογή - μπορείτε να αναδιαμορφώσετε τη σύνθεση σε κληρονομικότητα με λιγότερο πόνο από το αντίστροφο.

Ανάθεση με οκνηρή κατασκευή#

Ένα συχνό πρότυπο: ο συνεργάτης κοστίζει να χτιστεί, οπότε χτίστε τον στην πρώτη πρόσβαση. Με το σύγχρονο χαρακτηριστικό class:

use feature 'class';

class Logger {
    field $path :param;
    field $fh;

    method fh {
        unless ($fh) {
            open $fh, '>>', $path or die "open $path: $!";
        }
        return $fh;
    }

    method log ($msg) {
        print {$self->fh} $msg, "\n";
    }
}

Το fh είναι reader που χειρίζεται και την εφάπαξ κατασκευή. Οι καλούντες βλέπουν μια απλή μέθοδο· η οκνηρή cache είναι ιδιωτική.

Τι να μην κάνετε#

  • Πολλαπλή κληρονομικότητα για να μοιραστείτε δύο μεθόδους. Πληρώνετε το πλήρες κόστος του MRO για μια ασήμαντη νίκη, και ο επόμενος που θα κληρονομήσει από την κλάση σας κληρονομεί επίσης τον πόνο του MRO. Χρησιμοποιήστε έναν ρόλο ή ένα mixin.

  • Η AUTOLOAD ως αναθέτης. Βολική στο πεντάλεπτο demo, αδύνατο να ενδοσκοπηθεί, και τρώει τυπογραφικά. Γράψτε ρητούς προωθητές ή χρησιμοποιήστε handles.

  • Ρόλοι που φέρουν κατάσταση. Ένας ρόλος που ορίζει τόσο μεθόδους όσο και attributes είναι πραγματικά μια μικρή κλάση. Ο καθαρότερος ρόλος ορίζει συμπεριφορά που υποστηρίζει η κατάσταση της κλάσης-καταναλωτή· ο ρόλος απαιτεί τους accessors που χρειάζεται και εμπιστεύεται τον καταναλωτή ότι θα τους παράσχει. Το παράδειγμα χειροκίνητου mixin στο πρότυπο 1 το έκανε λάθος φτάνοντας απευθείας στο $_[0]->{is_on}.

  • Βαθιές αλυσίδες ανάθεσης. Αν το $player->radio->volume->level εμφανίζεται επανειλημμένα, προσθέστε έναν accessor volume_level στον Player. Οι αλυσίδες μεθόδων τύπου εκτροχιασμένου τρένου είναι μυρωδιά κακής σύνθεσης.

Περαιτέρω ανάγνωση#

  • Σύγχρονες κλάσεις - το βασικό στρώμα πάνω στο οποίο κάθεται η σύνθεση.

  • Κληρονομικότητα και επίλυση μεθόδων - όταν η απάντηση πραγματικά είναι η κληρονομικότητα.

  • Μετάβαση - πώς ο παλαιός κώδικας βασισμένος σε ρόλους μεταφράζεται στο μοντέλο class του πυρήνα (spoiler: στο μεγαλύτερο μέρος του αμετάβλητος· οι ρόλοι μεταφέρονται τελευταίοι).

  • class - αναφορά για τη σύγχρονη σύνταξη κλάσεων.

  • isa - έλεγχος ιδιότητας μέλους κλάσης· σε ζεύγος με το DOES για ιδιότητα μέλους ρόλου.