• בלוג
  • הגמישות במודל המחלקות של פרל (שלא מצאתי כמעט באף שפה אחרת)

הגמישות במודל המחלקות של פרל (שלא מצאתי כמעט באף שפה אחרת)

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

1. מחלקה היא בסך הכל אוסף של פונקציות הנקראות באופן מסוים

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

מחלקה היא אם כן בסך הכל הפשטה של המשותף לכל האובייקטים מאותו הסוג.

המימוש המקובל לאובייקטים ומחלקות הגיע עוד מ C++ ואולי אף קודם: שמרו בזכרון אובייקט מידע המכיל מצב פנימי ומצביעים לפונקציות השונות שאותן אפשר להפעיל עליו. בהפעלת פונקציה העבירו לה כפרמטר ראשון את אותו אובייקט מידע. כך קוד חיצוני יכול להגיע לפונקציות (שהרי הן מוצבעות מאובייקט המידע), וקוד פנימי יכול לגשת לאובייקט המצב (שהרי הוא הועבר לפונקציות כפרמטר ראשון).

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

package LightBulb;

sub turn_off {
    my ($self) = @_;
    if ( $self->{on} ) {
        $self->{on} = 0;
        print "Good night all\n";
    } 
}

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

2. הייחודיות של פרל: אתם מחליטים מהו this וכיצד יוצרים אחד

עד לכאן פרל לא שונה מפייתון או אפילו C++. אנו מציינים באופן מפורש את הפרמטר הראשון ונותנים לו את השם self, מה שב Java או C++ מתרחש באופן מובלע, אבל כך עושים גם בפייתון. המקום שפרל שונה מהשפות האחרות הוא הפונקציה new. במקומות אחרים מנגנון יצירת האובייקט הינו חיצוני למתכנת וכל שמתכנת יכול לכתוב זו פונקציית בנאי עבור אתחול האובייקט. פרל נותנת לכם חופש פעולה מלא באופן יצירת האובייקטים. בפרל כל Reference יכול להיות אובייקט ממחלקה. כדי להפוך משתנה לאובייקט ממחלקה כל מה שצריך זה ״לחבר״ אותו למחלקה. חיבור זה יכול להתבצע הרבה אחרי שנוצר האובייקט ומתוך כל פונקציה — באמצעות הפקודה bless.

הקוד הבא מציע מימוש סטנדרטי לפונקציית new:

sub new {
    my ($cls) = @_;
    my $self = { on => 0 };
    bless $self, $cls;
}

הפרמטר הראשון לפונקציה הוא שם המחלקה (מועבר אוטומטית). אנו מגדירים בתוך הבנאי אובייקט מידע מסוג Hash ומחברים אותו למחלקה. 

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

use strict;
use warnings;
use v5.18;

package RangeIterator;

sub new {
    my ($cls, $from, $to) = @_;

    my $self = sub {
        $from++;
        $from < $to ? $from    : undef;
    };
    bless $self, $cls;
}


package main;
my $i = RangeIterator->new(10, 15);
while (my $next = $i->()) {
    print $next, "\n";
}

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

3. דוגמא 1: סינגלטון

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

use strict;
use warnings;
use v5.18;

package Singleton;
my $instance;

sub new {
    my ($cls) = @_;
    if ( ! $instance ) {
        $instance = {};
        bless $instance, $cls;
    }
    return $instance;
}

package main;

my $p = Singleton->new;
$p->{foo} = 10;

my $r = Singleton->new;
print $r->{foo}, "\n";
# prints 10

באותו האופן אנו יכולים לבנות Pool של אובייקטים למשל עבור חיבור לבסיס נתונים. אפשר להתחיל לכתוב את המערכת עם חיבור חדש לכל הפעלה של new וכשרוצים לעבור לעבוד עם Pool של אובייקטים פשוט להחליף את הפונקציה new כך שתחזיר אובייקט מתוך המאגר במקום ליצור חדש.
(עבור מצב של Pool תצטרכו לדרוס גם את פונקציית המחיקה כדי לסמן שאובייקט חזר למאגר ואפשר לבצע בו שימוש חוזר).

4. דוגמא 2: פרוקסי

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

תבנית פרוקסי משמשת בעת יישום מנגנוני Lazy Loading ו Copy on Write. כך למשל עבור טעינה עצלה אין צורך להפעיל את הבנאי האמיתי בעת יצירת אובייקטים: אנו ניצור אובייקט דמה וברגע שקוד יקרא לאחת הפונקציות שלו, אז נחליף את אובייקט הדמה באובייקט האמיתי ונפעיל את הפונקציה. התוכנית הבאה עושה אפילו יותר מזה, היא תמתין עם טעינת קובץ המחלקה עד שמישהו ינסה להפעיל את אחת הפונקציות ממחלקה זו. כך לא נבזבז זמן וזכרון בטעינת מחלקות בהן אף אחד לא משתמש.

use strict;
use warnings;
use v5.18;

package Proxy;

sub new {
    my ($proxy, $cls) = @_;
    my $self = {
        cls => $cls
    };
    bless $self, $proxy;
}

sub AUTOLOAD {
    our $AUTOLOAD;
    my $method = pop [split "::", $AUTOLOAD];

    my ($self, @args) = @_;

    my $proxy_cls = ($self->{cls} =~ s{::}{/}rg);
    require $proxy_cls . ".pm";
    bless $self, $self->{cls};

    $self->$method;
}


package main;
use lib "/Users/ynonperek/proxy";

my $p = Proxy->new("Long::Class::Name::Hello");
my $t = Proxy->new("Foo");

$p->greet();

הקוד בדוגמא טוען את המחלקה Hello רק בשורה האחרונה בעת הפעלת הפונקציה, ואת המחלקה Foo אפילו לא מנסה לטעון. הקוד משתמש ב AUTOLOAD שזה מנגנון של פרל לביצוע קוד בעת נסיון להפעיל פונקציה שלא נמצאה במחלקה. דבר נוסף שעשוי להפתיע הוא החיבור מחדש של אותו האובייקט בתוך פונקציית AUTOLOAD. רצף האירועים בדוגמא הוא: 

  1. טעינת המחלקה Proxy.
  2. בניית שני אובייקטים חדשים מהמחלקה Proxy בשמות p ו t.
  3. נסיון להפעיל פונקציה על אובייקט p (שהמחלקה האמיתית שלו עדיין לא נטענה).
  4. טעינת המחלקה Hello, היא המחלקה האמיתית של אובייקט p.
  5. מיפוי מחדש של האובייקט כך שמעתה יתייחס למחלקה החדשה Hello.
  6. הפעלה של הפוקנציה מתוך המחלקה החדשה.

 

5. סיכום

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