תפקידים והאצלה#
ירושה עונה על ״מה העצם הזה?״ תפקידים והאצלה עונים על שאלה אחרת: ״מה העצם הזה יכול לעשות?״ כאשר שתי מחלקות לא קשורות צריכות לחלוק התנהגות, לכפות אותן תחת הורה משותף הוא הכלי הלא נכון. הרכבה - לתת לכל מחלקה את ההתנהגות שהיא צריכה בלי להעמיד פנים שהן קשורות - היא הכלי הנכון.
הפרק הזה מכסה את שלושת דפוסי ההרכבה שקוד pperl משתמש בהם בפועל:
שיתוף בסגנון mixin דרך חבילת עזר זעירה.
הרכבת תפקידים דרך
Role::Tinyאו מערכת ממשפחת Moose בקוד מורשת.האצלה - העברת מתודות לעצם מוכל.
התכונה class של הליבה עדיין לא מספקת פרימיטיב תפקידים. זוהי החלטת שלביות מכוונת במעלה־הזרם: התכונה class נחתה ראשונה, תפקידים הם הצעה נפרדת. עד שתכונת התפקיד של הליבה תישלח, הדפוסים בפרק הזה הם התשובות הפועלות.
הבעיה#
דמיינו מחלקת Radio ומחלקת Computer. לשתיהן יש מתג הפעלה/כיבוי. הן אינן קשורות - רדיו אינו מחשב, וגם אף אחד מהם אינו מתמחה בהיפותטית 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 }
זה עובד. זה גם שביר: הוא נכנס להאש של המחלקה הצורכת ישירות ($_[0]->{is_on}), שזה בדיוק הפרת ההכמסה ש־OO קלאסי מגובה־האש מפורסם בה. זה נשבר אם הצרכן הוא מחלקה שמוצהרת עם class, כי שדות אינם חריצי האש.
השתמשו בדפוס הזה רק להתנהגות משותפת קטנה מאוד בבסיס קוד קלאסי. לכל דבר גדול יותר, השתמשו במערכת תפקידים אמיתית.
דפוס 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 הידני נכשל בשקט בקריאת המתודה הראשונה.זיהוי קונפליקטים. צריכת שני תפקידים ששניהם מגדירים את אותה מתודה היא שגיאת זמן־הרכבה, ולא הגדרה־אחרונה־מנצחת בשקט.
מתאמי מתודה -
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.
אם התשובה ל״הוא־א או יש־לו־א?״ אינה מיידית, היחס הוא כנראה יש־לו־א. ברירת מחדל להרכבה היא כמעט תמיד הבחירה הפחות שגויה - אתם יכולים לעבד מחדש הרכבה לירושה בפחות כאב מההפוך.
האצלה עם בנייה עצלה#
דפוס נפוץ: משתף הפעולה יקר לבנייה, אז בנו אותו בגישה הראשונה. עם תכונת 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 שגם מטפל בבנייה החד־פעמית. קוראים רואים מתודה פשוטה; המטמון העצל פרטי.
מה לא לעשות#
ירושה מרובה כדי לחלוק שתי מתודות. אתם משלמים את עלות ה־MRO המלאה עבור ניצחון זעיר, והאדם הבא שיורש מהמחלקה שלכם יורש גם את כאב ה־MRO. השתמשו בתפקיד או ב־mixin.
AUTOLOADכמאציל. נוח בדמו של חמש דקות, בלתי אפשרי להתבונן פנימה, והוא בולע שגיאות הקלדה. כתבו מעבירים מפורשים או השתמשו ב־handles.תפקידים שנושאים מצב. תפקיד שמגדיר גם מתודות וגם תכונות הוא למעשה מחלקה־זעירה. התפקיד הנקי ביותר מגדיר התנהגות שמצב המחלקה הצורכת תומך בה; התפקיד דורש את ה־accessors שהוא צריך וסומך על הצרכן לספק אותם. דוגמת ה־mixin הידנית בדפוס 1 טעתה בעניין זה על־ידי הגעה ל־
$_[0]->{is_on}ישירות.שרשראות האצלה עמוקות. אם
$player->radio->volume->levelמופיע שוב ושוב, הוסיפו accessorvolume_levelעלPlayer. שרשראות מתודה בסגנון תאונת רכבת הן ריח של הרכבה.
קריאה נוספת#
מחלקות מודרניות - שכבת הבסיס שהרכבה יושבת מעליה.
ירושה ופתרון מתודות - כאשר התשובה באמת היא ירושה.
העברה - איך קוד מורשת מבוסס־תפקידים מתורגם למודל ה־
classשל הליבה (ספויילר: בעיקר ללא שינוי; תפקידים עוברים אחרונים).class- עיון לתחביר המחלקה המודרנית.isa- בדיקת חברות־במחלקה; שלבו עםDOESלחברות בתפקיד.