OO קלאסי#

לפני שהתכונה class נחתה ב־5.38, ה־OO של Perl נבנה משלושה פרימיטיבים: חבילה, הפניה, והפונקציה bless. שלושת הפרימיטיבים האלה עדיין נוכחים - מילת המפתח המודרנית class יושבת על אותו מנגנון - וכל בסיס קוד שקדם ל־5.38, וגם רוב CPAN, עדיין משתמשים בהם ישירות.

הפרק הזה נועד לקריאה ולתחזוקה של אותו הקוד. אם אתם כותבים מחלקה חדשה, התחילו במקום זאת עם פרק המחלקה המודרנית.

שלושת הפרימיטיבים#

  1. חבילה מגדירה מרחב שמות. כל חבילה יכולה להיות מחלקה; ההבדל הוא טהור בשימוש בה.

  2. הפניה מחזיקה נתוני מופע. המוסכמה היא הפניה להאש, אבל כל סוג הפניה עובד.

  3. bless מתייג את הישות המוצבעת בשם מחלקה, כך שדיספאץ« של מתודה דרך ההפניה מחפש subs בחבילה הזו.

זה כל המודל. כל השאר - בנאים, accessors, ירושה, תפקידים - הוא מוסכמה שבנויה מעל.

מחלקה מינימלית#

package File;

sub new {
    my ($class, %args) = @_;
    my $self = {
        path    => $args{path},
        content => $args{content},
    };
    return bless $self, $class;
}

sub path    { $_[0]{path} }
sub content { $_[0]{content} }

sub print_info {
    my $self = shift;
    print "This file is at $self->{path}\n";
}

1;

מוסכמות מפתח, כל אחת נושאת־עומס:

  • sub new הוא הבנאי. זו מתודה רגילה; אין בשפה מילת מפתח לבנאי. לפי מוסכמה הוא נקרא new ומחזיר את ההפניה המבורכת.

  • my ($class, %args) = @_ מחלץ קודם את ה־invocant, ואז את הארגומנטים בשם. קריאת מתודת־מחלקה File->new(path => ...) מעבירה את שם המחלקה כארגומנט הראשון.

  • bless $self, $class מצרף את תג המחלקה. שימוש ב־$class (ה־invocant בפועל) במקום 'File' קשיח הוא מה שהופך את הבנאי לבטוח־לירושה: תת־מחלקה שיורשת את new תבצע bless לשם של תת־המחלקה.

  • return בדרך כלל מושמט אחרי bless כי bless מחזיר את הארגומנט הראשון שלו. bless $self, $class כביטוי אחרון ב־sub הוא אידיומטי. אנחנו כותבים אותו במפורש כאן לקריאוּת.

מתודות accessor בכתיבה ידנית#

המודל לא נותן לכם accessors; אתם כותבים אותם. הצורה התמציתית מנצלת את העובדה ש־$_[0] הוא ה־invocant:

sub path    { $_[0]{path} }
sub content { $_[0]{content} }

צורת הפרמטר בשם ברורה יותר וזה מה שכדאי לכתוב כאשר ה־accessor עושה משהו לא־טריוויאלי:

sub path {
    my $self = shift;
    return $self->{path};
}

accessor לקריאה/כתיבה:

sub name {
    my $self = shift;
    $self->{name} = shift if @_;
    return $self->{name};
}

זהו ה־boilerplate שתכונת המחלקה המודרנית מחליפה עם :reader ו־:writer.

מדוע הפניות להאש#

הפניות להאש שולטות כי הן מאפשרות לכם להוסיף ולשנות שמות של שדות בלי לשנות את הפריסה, וכי מפתחות בעלי שם שורדים נסיעה חזור ב־Data::Dumper או ב־JSON::encode. הפניות למערך מהירות יותר וקומפקטיות יותר אבל שבירות: מיספור מחדש של חריץ שובר כל אתר גישה.

למופעים שמגובים בהאש יש חיסרון רציני אחד: אין הכמסה. כל קורא יכול להגיע לפנימיות של העצם עם $obj->{path}. למודל הקלאסי אין כלי לעצור אותם - משמעת עצמית וסקירת קוד הן ההגנות היחידות. זוהי הסיבה היחידה הגדולה ביותר להעדיף את תכונת המחלקה המודרנית עבור קוד חדש.

ירושה עם @ISA#

ירושה היא רשימה ברמת חבילה בשם @ISA. דיספאץ« של מתודה הולך עליה כאשר החבילה הנוכחית לא מגדירה את המתודה.

package File::MP3;
our @ISA = ('File');

sub print_info {
    my $self = shift;
    $self->SUPER::print_info;
    print "Title: $self->{title}\n";
}

1;

SUPER:: means ”look for this method starting one step up the ISA chain from the package the current method was compiled in

  • not from the object’s class. That subtlety matters in diamond inheritance; the inheritance chapter covers it.

הצהרת ההורה - ארבע דרכים#

ישנן ארבע איותים לאותו רעיון. העדיפו את השלישית עבור קוד חדש שאתם עדיין מתחזקים בסגנון הקלאסי.

# 1. Raw @ISA assignment.
package Child;
our @ISA = ('Parent');

# 2. use base.
package Child;
use base 'Parent';         # deprecated in modern Perl

# 3. use parent.
package Child;
use parent 'Parent';       # current recommendation for classical

# 4. @ISA with a require.
package Child;
require Parent;
our @ISA = ('Parent');

use parent מטפל בטעינת מודול ההורה ובדחיפה ל־@ISA בשורה אחת. use base הוא וריאנט ישן יותר שגם מייבא שדות בסגנון Exporter; הימנעו ממנו.

ה־invocant $self#

כל מתודה היא תת־שגרה שהארגומנט הראשון שלה הוא או שם המחלקה (עבור מתודות מחלקה כמו new) או העצם (עבור מתודות מופע). המוסכמה היא לבצע shift אליה למשתנה בשם $self:

sub method {
    my $self = shift;
    ...
}

או חלצו אותו יחד עם ארגומנטים אחרים:

sub method {
    my ($self, $arg1, $arg2) = @_;
    ...
}

אין אכיפה בזמן ריצה. אם אתם שוכחים לבצע shift על $self, הארגומנט הראשון שסיפק המשתמש הופך בשקט ל־$self והכאוס מגיע. מנתחים סטטיים תופסים את זה; השפה לא.

הורסים#

מתודה DESTROY, אם מוגדרת, רצה כאשר ה־refcount של העצם נופל לאפס. השתמשו בה לשחרר משאבים חיצוניים:

sub DESTROY {
    my $self = shift;
    close $self->{fh} if defined $self->{fh};
}

מלכודות שנושכות כל פרויקט לפחות פעם אחת:

  • DESTROY רץ בזמנים לא־צפויים. אל תסתמכו עליו לניקוי קריטי־לנכונות - $obj->close מפורש מנצח הריסה משתמעת.

  • DESTROY יכול לראות עצם שנבנה חלקית אם new קרס באמצע. בדיקות defined הגנתיות הן ביטוח זול.

  • במהלך הריסה גלובלית (זמן END), תלויות עשויות להיעלם כבר. DESTROY שהולך על הפניות יכול להיכשל עם ”Attempt to free unreferenced scalar“ או דומה. עטפו ניקוי מסוכן ב־eval { ... }.

AUTOLOAD#

אם קריאת מתודה לא מוצאת sub תואם במחלקה או באבותיה, Perl מחפש sub בשם AUTOLOAD. שם המתודה הנקראת מופיע ב־our $AUTOLOAD:

our $AUTOLOAD;
sub AUTOLOAD {
    my $self = shift;
    my $name = $AUTOLOAD;
    $name =~ s/.*:://;
    return if $name eq 'DESTROY';       # critical
    ...
}

שומר ה־DESTROY הוא חובה: בלעדיו, הריסת עצם מפעילה את AUTOLOAD עם $name = 'DESTROY' ואתם מתנהגים לא־כשורה או קורסים.

AUTOLOAD עוצמתי וקל לניצול לרעה. המקרים שבהם הוא מצדיק את קיומו:

  • ייצור עצל של accessor - סנתזו accessor בקריאה הראשונה, ואז התקינו אותו כ־sub אמיתי דרך השמת typeglob כך שהקריאה הבאה תהיה ישירה.

  • העברה - האציל מתודות לא־ידועות לעצם מוכל (אבל ראו תפקידים והאצלה לגישה נקייה יותר).

מקרים שבהם זה מזיק יותר מאשר עוזר: יומרה להיות MOP מלא, תפיסת שגיאות הקלדה כ־no-ops שקטים, יישום ״מחלקות״ שכל פני השטח שלהן הוא AUTOLOAD.

דוגמה מפותחת#

מחלקת Counter קלאסית עם accessor לקריאה/כתיבה וירושה:

package Counter;

sub new {
    my ($class, %args) = @_;
    my $self = {
        value => $args{value} // 0,
        step  => $args{step}  // 1,
    };
    return bless $self, $class;
}

sub value {
    my $self = shift;
    $self->{value} = shift if @_;
    return $self->{value};
}

sub step { $_[0]{step} }

sub tick {
    my $self = shift;
    $self->{value} += $self->{step};
}

1;

תת־מחלקה שמכפילה את הצעד:

package Counter::Double;
use parent 'Counter';

sub new {
    my ($class, %args) = @_;
    $args{step} //= 2;
    return $class->SUPER::new(%args);
}

1;

בהשוואה לגרסת המחלקה המודרנית, הצורה הקלאסית היא כפי שלוש שורות וכל אחת מהן היא מקום לטעות בו.

מתי OO קלאסי הוא הבחירה הנכונה#

למרות הכל, יש סיבות לגיטימיות להישאר עם מחלקות מבוססות bless:

  • אתם מתחזקים קוד קיים. לשכתב מחלקה קלאסית עובדת לצורת class הוא פרויקט העברה, לא ארוחת חינם - ראו את פרק ההעברה.

  • אתם צריכים לתמוך ב־Perl ישן יותר. אם הקוד שלכם חייב לרוץ על Perl לפני 5.38, התכונה class לא זמינה.

  • אתם צריכים עצמים מגובי־מערך או מגובי־קוד. class יוצרת רק מופעים מגובי־האש. אם אתם בונים מבנה קריטי־לביצועים עם מופעי הפניות־למערך, bless קלאסי הוא עדיין התשובה.

  • אתם עושים משהו שתכונת ה־class אוסרת במכוון - פתיחה מחדש של החבילה מבחוץ, ירושה מרובה, או metaprogramming עמוק דרך מניפולציה של @ISA.

הערה היסטורית על מערכות OO של CPAN#

מכיוון שהמודל הקלאסי משאיר כל כך הרבה למוסכמה, ב־CPAN צמחה מערכת אקולוגית של מודולים ששכבת תחביר הצהרתי מעל:

  • Moose - מערכת OO מלאת־תכונות עם מערכת טיפוסים מזערית, תפקידים, מתאמי מתודה, API התבוננות פנימית מלא, ומערכת הרחבות גדולה. כבדה בזמן טעינה; הבחירה הדומיננטית עבור יישומים גדולים שנבנו בין 2007 לבערך 2020.

  • Moo - תת־קבוצה של ה־API של Moose ללא שכבת ההתבוננות הפנימית. קלה יותר, Perl טהור, תואמת API מספיק כדי לאפשר למחלקת Moo ומחלקת Moose לחלוק עץ ירושה.

  • Class::Accessor - מייצרת accessors פשוטים ו־new. מינימלית, Perl טהור, ללא תפקידים.

  • Class::Tiny - הקטנה ביותר ממודולי מחוללי ה־accessor. כל ה־accessors הם לקריאה/כתיבה, ללא בדיקות טיפוס.

  • Object::Pad - הפרוטוטיפ הניסיוני שהפך לתכונת class של הליבה. עדיין ניתן להתקנה ב־CPAN עבור קוד הזקוק לתכונה על Perls ישנים יותר.

  • Role::Tiny - הרכבת תפקידים לפרויקטים שאינם משתמשים ב־Moose אך עדיין רוצים שיתוף בסגנון does.

המערכות האלה הן הקשר היסטורי ב־pperl. קוד חדש ב־pperl צריך להשתמש בתכונת class של הליבה, המכסה את הקרקע ש־Moose/Moo פתחו ללא עלות זמן הטעינה והתלות. כאשר אתם יורשים בסיס קוד שמשתמש באחד מהם, למדו רק מספיק מפני השטח שלו כדי להישאר פרודוקטיביים ותכננו את ההעברה עם פרק ההעברה.