העברה מהקלאסי למודרני#
אם אתם מתחזקים בסיס קוד מבוסס־bless ותוהים איך נראה בלוק class שקול, הפרק הזה הוא המתכון. ההמרה היא בעיקרה מכאנית, אבל המקומות שבהם היא אינה מכאנית הם המקומות שבהם החלטות עיצוב אמיתיות צפות - ושבהם ההעברה משתלמת לעצמה.
לפני שתתחילו, קראו את פרק המחלקה המודרנית לתחביר שאתם מתרגמים אליו, ועברו ברפרוף על פרק ה־OO הקלאסי לכל מבנה שהקוד הקיים משתמש בו שאולי לא תזכרו בפירוט.
מתי להעביר#
ההעברה אינה חינמית. בצעו אותה כשאתם מקבלים ערך אמיתי:
המחלקה נמצאת תחת פיתוח פעיל. אתם עומדים להוסיף שדות, להוסיף מתודות, או לשנות את חוזה הבנאי. ביצוע העבודה הזו מול תכונת
classנוטה פחות לשגיאות מאשר ביצועה מול חריצי האש שנכתבו ביד.למחלקה יש הכמסה לא־ברורה. קוראים מגיעים אל
$obj->{fieldname}ישירות, ואתם רוצים לעצור זאת מבלי לשכתב כל אתר קריאה בבת אחת. תכונתclassהופכת את ההפרה לשגיאת זמן־הידור ברגע שאתם מחליפים.המחלקה יושבת על Moose או Moo, ואתם משלמים את עלות זמן הטעינה מבלי להשתמש בתכונות הכבדות־בתלויות. ה־
classהמודרני הוא החלפה ישירה לרוב המחלקות של Moose/Moo המשתמשות רק ב־hasוב־with.
אל תעבירו מחלקה שעובדת וקפואה. מחלקה קלאסית יציבה שלא תראה קוד חדש למשך עוד חמש שנים היא בדיוק כמו השקולה המודרנית שלה, וההעברה עצמה תכניס באגים שלמחלקה היציבה אין.
ההמרה המכאנית#
קחו את המחלקה הקלאסית הזו:
package Counter;
sub new {
my ($class, %args) = @_;
my $self = {
value => $args{value} // 0,
step => $args{step} // 1,
};
die "step must be positive" if $self->{step} <= 0;
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;
הצורה המודרנית:
use v5.38;
use feature 'class';
no warnings 'experimental::class';
class Counter {
field $value :param :reader :writer = 0;
field $step :param :reader = 1;
ADJUST {
die "step must be positive" if $step <= 0;
}
method tick { $value += $step }
}
1;
המיפוי, חלק־חלק:
מבנה קלאסי | מבנה מודרני |
|---|---|
|
|
| (נמחק - מיוצר) |
ברירת מחדל לארגומנט בנאי עם |
|
|
|
accessor לקריאה בלבד |
|
accessor לקריאה/כתיבה |
|
תיקוף בסוף | בלוק |
|
|
| (נמחק - משתמע) |
|
|
| ללא שינוי - עדיין עובד |
החלקים הלא־מכאניים#
כל מה שלמעלה הוא תרגום שגרתי. המקומות שבהם אתם צריכים לחשוב הם כולם וריאנטים של ״הקוד הקלאסי עשה משהו שתכונת class לא עושה באותו אופן.״
ירושה מרובה#
class אינו תומך ב־:isa(A, B). אם למחלקה הקלאסית יש @ISA = ('Parent1', 'Parent2'), יש לכם שלוש אפשרויות:
צמצמו לירושה יחידה. בדרך כלל ההורה השני הוא mixin או תפקיד בתחפושת. העבירו אותו לתפקיד וצרכו אותו עם
with.צמצמו להרכבה. אם ההורה השני הוא משתף פעולה ולא אב, העבירו להאצלה במקום ירושה.
אל תעבירו את המחלקה. אם הירושה המרובה היא באמת נושאת־עומס ואף אחד מהאמורים לעיל לא מתאים, המחלקה אינה מתאימה היטב לתכונה המודרנית. השאירו אותה קלאסית עד שתכונת התפקיד של הליבה תיחת.
בפועל אפשרות 1 או 2 חלה על כ־95% ממקרי הירושה המרובה בקוד אמיתי.
גישה ישירה להאש מהקוראים#
אם קוראים חיצוניים עושים $counter->{value} במקום $counter->value, העברת המחלקה תשבור אותם. שגיאת המהדר נקייה ומקומית - הודעת השגיאה נוקבת באתר - אבל ההעברה כבר אינה drop-in.
אסטרטגיית הקלה:
בדיקה ראשונה. בצעו grep לדפוסי
$obj_name->{ברחבי בסיס הקוד. הספירה לרוב קטנה.הוסיפו accessor למחלקה הקלאסית שמכסה כל שדה שנגישים אליו.
החליפו אתרי קריאה של
$obj->{fieldname}ב־$obj->fieldname()ב־commit ייעודי.ואז העבירו את המחלקה לצורה המודרנית.
פיצול הריפקטור לשני שלבים שומר על כל commit קטן ובר־bisect.
AUTOLOAD#
ל־AUTOLOAD במחלקה קלאסית אין שקול מודרני. תכונת class לא נותנת לכם לתפוס קריאות מתודה חסרות.
אם ה־AUTOLOAD עושה אחד מהדפוסים הנפוצים, תרגמו אותו:
סינתזה עצלה של accessors - הצהירו על ה־accessors במפורש כשדות עם
:readerאו:writer. ייצור הקוד של תכונתclassמחליף את מה ש־AUTOLOADהיה עושה.האצלה לעצם מוכל - החליפו במתודות מעבירות מפורשות (ראו תפקידים והאצלה) או ב־accessor
handlesבסגנון Moose אם בסיס הקוד יכול להיות תלוי ב־Moose.proxy גנרי - שמרו את המחלקה קלאסית. תכונת
classאינה הכלי הנכון.
DESTROY#
class אינו מציג צורת הורס מובנית בשפה. אם למחלקה הקלאסית יש DESTROY, כתבו אותו כמתודה רגילה בתוך בלוק ה־class - הוא מופעל דרך אותו הוו של refcount:
class Logger {
field $fh;
ADJUST { ... }
sub DESTROY {
my $self = shift;
close $self->{fh} if $self->{fh};
}
}
שימו לב לפגם: בתוך DESTROY אתם לא יכולים לנקוב בשדה לפי שמו המוצהר, כי גופי sub אינם רואים קישורי שדות. השתמשו במתודה רגילה ש־DESTROY מאציל אליה, או קבלו את האסימטריה הקלה.
דפוס נקי יותר כאשר אורך החיים בשליטתכם הוא להוסיף מתודה close מפורשת ולקרוא לה דטרמיניסטית, ולהשאיר את DESTROY כגיבוי חגורה־וכתפיות.
מופעים לא־האשיים#
אם המחלקה הקלאסית מבצעת bless \@self, $class או bless \$self, $class - מופע מגובה־מערך או מגובה־סקלר - לתכונה המודרנית אין שקול. פריסת מופע דמוית־האש אפויה פנימה. שמרו את המחלקות האלה קלאסיות.
האשים קשורים / overloading#
שילובי use overload ו־tie עובדים ללא שינוי בבלוק class. אין מה לעשות במיוחד.
העברת מחלקות Moose / Moo#
ההמרה מ־Moose ל־class נקייה יותר מהקלאסית כי Moose כבר הצהרתי. המפה הגסה:
Moose / Moo |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| (חכו לתפקידים של הליבה) |
| (אין שקול ישיר) |
| (אין שקול ישיר) |
פריטי הפער - אילוצי טיפוס, מתאמי מתודה, תפקידים - הם מה ששומר בסיסי קוד של Moose על Moose בינתיים. אם השימוש שלכם ב־Moose מוגבל ל־has, extends, ו־BUILD, ההעברה ישירה. אם הוא תלוי בכבדות במערכת הטיפוסים של Moose או במתאמי המתודה, ההעברה היא פרויקט, לא commit.
בדיקת ההעברה#
העברת מחלקה שאינה משנה התנהגות חיצונית צריכה לעבור את חבילת המבחנים הקיימת ללא שינוי. צעדים:
ודאו שלמחלקה יש מבחנים המכסים את ה־API הציבורי שלה. אם אין, כתבו אותם לפני ההעברה - המבחנים הם רשת הביטחון של ההעברה שלכם.
העבירו את המחלקה.
הריצו את המבחנים. מעבר נקי הוא איתות ההשלמה.
חפשו ב־grep בבסיס הקוד אחר
$obj->{...}המגיע אל מופעי המחלקה שהועברה. תקנו כל פגיעה.הסירו כל
use parent,use base,sub newמפורש, או boilerplate של accessor שעכשיו מתים שהצורה הישנה צרכה.
אם צעד 3 נכשל, הכשלים נופלים בדרך כלל לרשימה קצרה:
גישה ישירה להאש במתודה שהצורה הישנה סבלה והצורה החדשה דוחה. החליפו בהפניות לשדה.
ארגומנט בנאי שהתקבל בשקט. הבנאי המיוצר קפדן לגבי שמות
:param. הצהירו על הפרמטר או שנו את אתר הקריאה.מאתחל שדה שהפנה ל־
$self. מאתחלי שדה רצים לפני ש־$selfנקשר. העבירו את הלוגיקה התלויה לבלוקADJUST.
מה אתם מקבלים מזה#
ההעברה מהקלאסי למודרני יוצאת דופן בכך שהתוצאה היא גם קצרה יותר וגם נכונה יותר:
קצרה יותר. דוגמת ה־
Counterשלמעלה יורדת מ־~20 שורות ל־~10, וכל השורות שהוסרו הן boilerplate.שדות פרטיים. קוד חיצוני כבר לא יכול להגיע פנימה. ההכמסה שהייתם מגנים עליה בסקירה מוגנת כעת על־ידי המהדר.
אין
blessשנשכח. הבנאי המיוצר תמיד בטוח־לירושה; אתם לא יכולים לשלוח בנאי שמבצע bless בטעות למחלקה הלא נכונה.אין
$self = shiftשנשכח. הקישור המשתמע לא יכול להיעלם.דגל תכונה בעל שם.
no warnings 'experimental::class'מתעד בראש הקובץ במה המחלקה תלויה.
קריאה נוספת#
מחלקות מודרניות - עיון לתחביר שאתם מעבירים אליו.
OO קלאסי - עיון לתחביר שאתם מעבירים ממנו.
ירושה ופתרון מתודות - פרק ההיררכיה המעורבת, שימושי בעוד בסיס הקוד מועבר חצי.
תפקידים והאצלה - לאן ירושה מרובה מגיעה בקוד שהועבר.
class,field,method- דפי העיון לשלוש מילות המפתח בלב ההעברה.bless,ref,isa- הפרימיטיבים שמסייעים גם לצורה הישנה וגם לחדשה ושממשיכים לעבוד ברחבי היררכיות מעורבות.