Некоторые сведения о Perl 5/Объектно-ориентированное программирование в Perl

Материал из Викиучебника — открытых книг для открытого мира
← Пакеты, библиотеки и модули Глава Работа со строками и регулярные выражения →
Объектно-ориентированное программирование в Perl


В языке Perl 5 нет специального синтаксиса для описания классов. Для их реализации используются существующие синтаксические конструкции.

Класс в Perl представляет собой пакет, а функции в пакете представляют его методы. Таблица символов класса представляет его содержимое — список полей и методов.

Объект в Perl — это ссылка на экземпляр класса, которая строится с помощью специальной функции класса — конструктора. Функция ref() возвращает для ссылки на объект не стандартный идентификатор типа SCALAR, ARRAY и др., а возвращает имя класса, к которому принадлежит объект.

В общих чертах, конструктор любого класса для конструирования ссылки на объект должен вызывать функцию

bless <ссылка> [, <имя-класса>]

которая «благословляет» (англ. to bless — благословлять) ссылку на то, чтобы принадлежать к классу, указанному вторым аргументом. Если имя класса опущено, то используется имя текущего пакета. Именно ссылка, возвращаемая этим методом, должна возвращаться из конструктора. После того как объект построен, к нему можно обращаться через квалифицированное имя $<имя-класса>::<ссылка>.

Наследование в Perl еще сильнее отличается от того, что есть в других языках программирования. В Perl наследуются только методы. Наследование данных реализует сам программист. Наследование методов реализовано так: с каждым пакетом ассоциирован свой массив @ISA, в котором хранится список базовых классов пакета. Если внутри класса вызывается метод, которого нет в текущем классе, то интерпретатор в поисках метода просматривает по порядку методы каждого класса массива @ISA. Затем просматривается предопределенный класс UNIVERSAL. Если и после этого метод не был найден, то классы снова просматриваются в том же порядке в поисках процедуры AUTOLOAD. Если такая находится, то вызывается она вместо отсутствующего метода. В эту процедуру будут переданы все параметры, а в переменной $AUTOLOAD будет хранится имя вызываемого метода.

Объявление класса[править]

Конструктор[править]

Конструктор класса является простой функцией пакета. Она может иметь любое имя, но на практике обычно используют идентификатор new, как устоявшееся имя для любого конструктора.

Самый простой конструктор имеет следующий вид:

package MyClass;

sub new {
    my $class = shift;   # Передаем конструктору имя класса
    my $self = {};       # Конструируем ссылку
    bless $self, $class; # Конструируем объект
    return $self;        # Возвращаем ссылку
}

# ---------------------------------------------
package main;

# Конструируем объект
$object = MyClass::new("MyClass");

Для примера мы объявляем класс и конструируем его экземпляр в одном исходном файле. Здесь мы это делаем только для демонстрации: на практике классы следует объявлять в отдельных исходных файлах и подключать их через функцию use(). Имя ссылки $self является устоявшимся, и мы настоятельно рекомендуем использовать ее в своих проектах.

Данный пример демонстрирует пустой бесполезный класс, в котором нет полей, так как мы конструируем объект из ссылки на пустой хеш. На практике обычно в конструктор передается массив или другой хеш, которым инициализируется ссылка $self. В общем случае не обязательно использовать именно хеш, но его преимущество в том, что мы можем использовать произвольные ключи. Также мы можем передавать массив значений, которыми затем мы можем инициализировать анонимный хеш. Другими словами, подходы здесь могут быть самые разные.

Деструктор[править]

Когда ссылка на объект выходит из зоны видимости, она удаляется по нулевому счетчику ссылок. Программист может контролировать действия, которые исполнятся перед очисткой ссылки, через деструктор объекта. Деструктор объекта нужен, чтобы корректно завершить жизненный цикл объекта, например, закрыть открытые объектом файловые дескрипторы. Деструктор всегда вызывается автоматически.

Деструктор всегда должен быть объявлен внутри класса и всегда должен иметь имя DESTROY. Также деструктор должен принимать аргумент в виде ссылки на удаляемый объект.

Деструктор является необязательным методом объекта, так как ссылка на удаляемый объект в любом случае будет очищаться правильно. Данный метод позволяет вам лучше контролировать этот процесс. Деструктор не вызывает автоматически другие деструкторы, другими словами, если ваш класс наследует другой, деструктор базового класса нужно вызвать явно в деструкторе дочернего.

Ниже приведен простой пример деструктора.

package MyClass;

sub new {
    my $class = shift;   # Передаем конструктору имя класса
    my $self = {};       # Конструируем ссылку
    bless $self, $class; # Конструируем объект
    return $self;        # Возвращаем ссылку
}

sub DESTROY {
    my $name = ref($_[0]);
    print "Call destructor to delete an instance of " . $name . " class\n";
}

# ---------------------------------------------
package main;
{
    my $object = MyClass::new("MyClass");
} # Деструктор объекта в блоке вызывается в этой точке.

$object = MyClass::new("MyClass");
# Деструктор объекта, созданного в области видимости пакета main удаляется перед завершением программы.

Если вызвать программу, можно убедиться, что деструктор был вызван для каждого из двух объектов:

Call destructor to delete an instance of MyClass class   
Call destructor to delete an instance of MyClass class

Другие методы[править]

Методы в Perl могут вызываться от имени конкретного объекта или от имени всего класса. В последнем случае такие методы называются статическими. Статическому методу в первом аргументе всегда передается имя класса, а не статическому — ссылка на объект. Типичным примером статического класса является конструктор (именно поэтому мы ожидаем в первом аргументе имя класса).

В Perl методы выполняются в пространстве имен того пакета, в котором они были определены, а не в том, в котором они вызываются. Именно поэтому многие статические методы игнорируют свой первый аргумент.

Не статические методы выполняют самую разную работу. Обычно эти действия направлены на инкапсулированные в объекте данные. В Perl полностью инкапсулировать данные невозможно, так как вы можете при желании исправлять их прямо через ссылку на объект (если конечно знаете как он устроен). На практике всегда следует изменять инкапсулированные данные через методы класса, игнорируя то, что сам объект является простой ссылкой, так как в этом и есть ООП подход.

package Person;

$classCounter = 0;

# Конструктор
sub new {
    my ($class, $data) = @_;   # Передаем конструктору имя класса вместе с ссылкой на структуру
    my $self = $data;
    bless $self, $class;       # Конструируем класс
    $classCounter++;
    return $self;        
}

# Деструктор
sub DESTROY {
    my $name = ref($_[0]);
    print "Call destructor to delete an instance of " . $name . " class:\n";
    while ( my($k,$v) = each %{$_[0]} ) {
        print "\t$k => $v\n";
    }
    $classCounter--;
}

# Простой сеттер, устанавливающий имя персоны
sub setName {
    my ($self, $data) = @_;
    $self->{'name'} = $data;
    return $self;
}

# Простой геттер, возвращающий поле 'name'
sub getName {
    my $self = shift;
    return $self->{'name'};
}

sub setSurname {
    my ($self, $data) = @_;
    $self->{'surname'} = $data;
    return $self;
}

sub getSurname {
    my $self = shift;
    return $self->{'surname'};
}

# Данный метод может вызываться как статический.
sub show {
    my $self = shift;
    my @keys = @_ ? @_ : sort keys %$self;
    foreach $key (@keys) {
        print "\t$key => $self->{$key}\n";
    }
    return $self;
}

# Чисто статический метод
sub count {
    my $class = shift;
    print "Counter of $class: $classCounter\n";
}

# ---------------------------------------------
package main;

# Конструируем два объекта
$person1 = Person::new(Person, {name => 'Larry', surname => 'Wall'});
$person2 = Person::new(Person, {});

print "-- 1 --\n";
$person1->show();                # вызов метода
print "-- 2 --\n";
$person1->show('name');          # печатаем одно поле структуры
$person1->setName('Garry');      # меняем поле структуры
print "-- 3 --\n";
Person::show($person1);          # вызываем статический метод
print "-- 4 --\n";
$person1->count();               # вызываем метод, считающий объекты
Person::count();
Person::count('Person');
print "-- 5 --\n";
$person2->setName('Homer');      # вызываем сеттеры
$person2->setSurname('Simpson');
$person2->show();
print "-- 6 --\n";
print STDOUT $person1->getName(), "\n";  # вызываем геттер
print "-- 7 --\n";
# строим объект, чтобы увеличить счетчик
{
    local $person1 = Person::new(Person, {});
    Person::count('Person');   # 3
}
Person::count('Person'); # 2

Результат работы программы

-- 1 --
        name => Larry
        surname => Wall
-- 2 --
        name => Larry
-- 3 --
        name => Garry
        surname => Wall
-- 4 --
Counter of Person=HASH(0x55cc685f3470): 2   # Статический метод, вызванный от имени объекта
Counter of : 2                              # Без аргумента метод ведет себя как статический
Counter of Person: 2
-- 5 --
        name => Homer
        surname => Simpson
-- 6 --
Garry
-- 7 --
Counter of Person: 3                         # Нарастили счетчик ссылок
Call destructor to delete an instance of Person class:
Counter of Person: 2                         # Объект был удален - счетчик уменьшился
Call destructor to delete an instance of Person class:
        surname => Wall
        name => Garry
Call destructor to delete an instance of Person class:
        surname => Simpson
        name => Home

Методы класса можно вызывать двумя способами. Первый способ имеет вид

<имя-метода> <имя-класса-или-объект>, <параметры>;

print "-- 1 --\n";
$person = Person::new 'Person', {name => "Larry", surname => 'Wall'};
Person::show $person, 'name', 'surname';
print "-- 2 --\n";
# В общем случае метод может распечатать любую ссылку
Person::show {name => 'Dennis', surname => 'Ritchie', born => '1941'};
print "-- 3 --\n";
Person::show Person::new 'Person', {name => 'Dennis', surname => 'Ritchie', born => '1941'};
print "-- 4 --\n";
print STDOUT Person::getSurname Person::new 'Person', {name => 'Dennis', surname => 'Ritchie', born => '1941'};
-- 1 --
        name => Larry
        surname => Wall
-- 2 --
        born => 1941
        name => Dennis
        surname => Ritchie
-- 3 --
        born => 1941
        name => Dennis
        surname => Ritchie
Call destructor to delete an instance of Person class:
        surname => Ritchie
        born => 1941
        name => Dennis
-- 4 --
RitchieCall destructor to delete an instance of Person class:
        name => Dennis
        surname => Ritchie
        born => 1941
Call destructor to delete an instance of Person class:
        surname => Wall
        name => Larry

Данная форма представляет собой вызов функции, вложенный в другой вызов.

Второй формой мы пользовались в самом первом примере этого раздела:

<класс-или-объект>-><метод>(<параметры>)

$person = Person->new({name => 'Larry', surname => 'Wall', born => '1954'});
print "-- 1 --\n";
$person->show('name', 'born');
print "-- 2 --\n";
Person->new({name => 'Larry', surname => 'Wall', born => '1954'})->show('name', 'surname');
-- 1 --
        name => Larry
        born => 1954
-- 2 --
        name => Larry
        surname => Wall
Call destructor to delete an instance of Person class:
        name => Larry
        born => 1954
        surname => Wall
Call destructor to delete an instance of Person class:
        name => Larry
        born => 1954
        surname => Wall

Вторая форма требует обязательного заключения аргументов в скобки. В большинстве случаев вторая форма предпочтительнее в смысле читаемости текста программы, так как оператор разыменовывания наглядно отделяет части выражения. При использовании первой формы могут возникнуть трудноуловимые ошибки, особенно когда действует область видимости, в которой существует функция с тем же именем, что и у метода класса.

Наследование[править]

Для реализации механизма наследования в Perl используется специальный массив @ISA, в который помещаются родительские классы слева направо от ближайшего к дальнему предку. Если интерпретатор встречает вызов метода, которого нет в данном классе, то он сначала пройдется по классам из @ISA в поисках метода.

Если вам нужно вызвать метод родительского класса из дочернего класса, вы можете воспользоваться квалифицированным вызовом метода. Если вам нужен метод ближайшего предка, вы можете использовать имя псевдокласса SUPER (но только из методов потомка).

package Parent;

sub new {
    my ($class, $data) = @_;
    my $self = $data;
    bless $self, $class;
    return $self;
}

sub DESTROY {
    print "Call destructor to delete an instance of 'Parent' class\n";
}

sub set {
    my ($self, $data) = @_;
    for $i (keys %$data) {
        $self->{$i} = $data->{$i};
    }
    return $self;
}

sub show {
    my $self = shift;
    my @keys = @_ ? @_ : sort keys %$self;
    print "Parent:\n";
    foreach $key (@keys) {
        print "\t$key => $self->{$key}\n";
    }
    return $self;
}

sub AUTOLOAD {
    print "Package " . __PACKAGE__ . ": there is not '$AUTOLOAD' method\n";
}

package Child;

@ISA = (Parent);

sub new {
    my ($class, $data) = @_;
    my $self = Parent->new($data);
    $self->{"born"} = 0;
    bless $self, $class;
    return $self;
}

sub DESTROY {
    my $self = shift;
    print "Call destructor to delete an instance of '" . ref($self) . "' class\n";
    # Вызываем деструктор родительского класса
    $self->SUPER::DESTROY;
}

# Переопределяем метод
sub show {
    my $self = shift;
    print "Child:\n";
    $self->SUPER::show();
    return $self;
}

package main;

$person = Child->new({name => 'Larry', surname => 'Wall'});
print "-- 1 --\n";
$person->show();
print "-- 2 --\n";
$person->set({born => 1954});
$person->show();
print "-- 3 --\n";
$person->Parent::show();
print "-- 4 --\n";
$person->undefinedMethod;
-- 1 --
Child:
Parent:
        born => 0
        name => Larry
        surname => Wall
-- 2 --
Child:
Parent:
        born => 1954
        name => Larry
        surname => Wall
-- 3 --
Parent:
        born => 1954
        name => Larry
        surname => Wall
-- 4 --
Package Parent: there is not 'Child::undefinedMethod' method
Call destructor to delete an instance of 'Child' class
Call destructor to delete an instance of 'Parent' class

В данном примере мы имеем два класса Parent и Child. Родительский класс объявляет два метода: метод Parent::set() нужен, чтобы устанавливать поля структуры класса; метод Parent::show() распечатывает все поля структуры класса.

Дочерний класс наследует метод Parent::set(), поэтому мы можем вызывать его без квалификатора типа, и переопределяет метод Parent::show(). Также дочерний класс в своем конструкторе вводит поле born, которое по умолчанию инициализируется нулевым значением.

Обратите внимание, как мы определяем методы дочернего класса: все они вводят небольшие изменения в алгоритм, но все равно вызывают методы родительского класса. На практике не всегда нужно поступать так, но конструкторы и деструкторы по логике вещей всегда должны обращаться к конструкторам и деструкторам предков, причем конструирование должно происходить сверху вниз (от дальнего предка к последнему потомку), а для деструкторов снизу вверх (от дальнего потомка к дальнему предку).

В последнем операторе мы пытаемся вызывать несуществующий метод из объекта дочернего класса. Так как в родительском классе объявлен метод AUTOLOAD, то будет вызван именно он.

Использование таблицы символов для генерации методов на ходу[править]

Некоторые стандартные методы, такие как геттеры и сеттеры, могут быть сгенерированы по контексту самой структуры класса. Для этого мы можем пройтись по полям структуры класса и сгенерировать из имен полей стандартные методы устанавливающие и возвращающие значения.

Внимательно изучите следующий пример.

package Person;

sub new {
    my ($class, $params) = @_;
    my $self = {};
    # В этом цикле мы генерируем методы через псевдонимы и анонимные функции.
    # Геттеры и сеттеры генерируются каждый раз при вызове конструктора, что является недостатком подхода.
    # Чтобы исправить недостаток, нужно добавить проверку на существование метода, но в этом примере мы это опустим.
    # Для геттеров используются имена get_<имя-поля>, а для сеттеров – set_<имя-поля>.
    for my $key (sort keys %$params) {
        *{__PACKAGE__ . '::' . "get_$key"} = sub {
            my $self = shift;
            return $self->{$key};
        };
        *{__PACKAGE__ . '::' . "set_$key"} = sub {
            my ($self, $data) = @_;
            $self->{$key} = $data;
            return $self;
        };
        $self->{$key} = $params->{$key};
    }
    bless $self, $class;
    return $self;
}

package main;

$person = Person->new({name => 'Larry', surname => 'Wall'});
print $person->get_name() . " " . $person->get_surname, "\n";
$person->set_name('Garry');
print $person->get_name() . " " . $person->get_surname, "\n";
Larry Wall
Garry Wall

В примере выше мы генерируем как будто бы пустой класс. На самом деле он генерирует свои методы на ходу, а именно при вызове конструктора. Для этого он для каждого поля хеша, передаваемого в конструктор, генерирует анонимную функцию, к которой можно обращаться по ссылке. Для геттеров имя ссылки будет get_<имя-поля>, а для сеттеров — set_<имя-поля>.

Этот пример демонстрирует, что в принципе в Perl можно создавать очень сложные абстрактные классы, но вероятнее всего вам не захочется этим заниматься.

Шаблон модуля, несущего в себе класс[править]

package MyClass;

use strict;                              # Для чистоты кода

require Exporter;                        # Для функции import()
our @ISA = qw{ Exporter };               # Добавьте родительские классы, если нужно
our @EXPORT = qw{ new method1 method2 }; # Добавьте публичные методы класса

sub new {
    my ($class, $data) = @_;
    my $self = $data;
    bless $self, $class;
    return $self;
}

sub method1 {
    my ($self, $data) = @_;
    # ...
}

sub method2 {
    my $self = shift;
    my $data = shift;
    # Примечание: shift возвращает очередной аргумент и сдвигает строку аргументов налево
    # ...
}

sub private {
    my $self = shift;
    # ...
}

1;
__END__

Документация

В заключение данного раздела[править]

Описанный в данном разделе подход к ООП не является единственным. Существуют модули CPAN, которые пытаются улучшить эту систему, обогащая описанные выше скудные конструкции. Такие модули, как правило, используются в больших проектах.

Однако, как бы там ни было, CPAN модули могут требовать загрузки множества зависимостей и могут замедлять скорость исполнения программы из-за своей внутренней реализации. В маленьких проектах достаточно встроенного в Perl ООП.



← Пакеты, библиотеки и модули Работа со строками и регулярные выражения →