NAME/НАИМЕНОВАНИЕ

perlootut - Руководство по Объектно-ориентированному Программированию в Perl

ДАТЫ

Этот документ был создан в феврале 2011 года и последняя на данный момент основная ревизия - февраль 2013 года.

Если вы читаете эти строки, находясь в будущем, вполне возможно, что описываемые здесь вещи изменились. Поэтому рекомендуем вам читать perlootut последней стабильной версии Perl.

ОПИСАНИЕ

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

Встроенная в Perl ОО система по умолчанию крайне минималистична, предоставляя вам самому выполнить большую часть работы. Этот минимализм имел много смысла в 1994, но в годы, последовавшие за появлением Perl 5.0, мы наблюдали появление ряда распространенных паттернов ОО в Perl. По счастью, Perl гибок, что позволяет процветать развитой ОО экосистеме.

Если вам интересно, что находится под капотом ОО в Perl, подробные детали описываются в perlobj.

В этом руководстве подразумевается, что вы понимаете основы синтаксиса Perl, типы переменных, операторы и вызов подпрограмм. Если нет, начните с perlintro. Необходимо также ознакомиться с perlsyn, perlop и perlsub.

ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПОДХОДА

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

ОО в Perl основана на классах и такой подход используется во многих ОО системах - в Java, C++, C#, Python, Ruby и еще куче языков. Есть и другие парадигмы объектно-ориентированной архитектуры. Например, один из самых популярных языков программирования JavaScript использует парадигму, в основе которой лежат прототипы.

Объект

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

Объект представляет единственную дискретную сущность. Например, объект может представлять файл. Атрибуты файла-объекта могут включать путь, содержимое и время последней модификации. Если мы создадим объект, представляющий файл /etc/hostnames на машине "foo.example.com", то атрибут путь этого для этого объекта будет иметь значение"/etc/hostname", его содержимое будет "foo\n" и время последнего изменения будет 1304974868 в секундах, прошедших с начала эпохи.

С файлом могут быть связаны методы rename и write.

Большинство объектов в Perl являются хешами, но ОО система рекомендует не полагаться на это знание. На практике лучше рассматривать внутреннюю структуру данных объекта, как непрозрачную.

Классы

Класс определяет поведение категории объектов. Класс является названием категории (например, "File") и задает поведение объектов этой категории.

Любой объект принадлежит определенному классу. Например, объект /etc/hostname принадлежит классу File. Когда мы хотим создать определенный объект, мы обращаемся к классу и конструируем или создаем экземпляр объекта. Конкретный объект часто называют экземпляром класса.

В Perl любой пакет может быть классом. Разница между пакетом, являющимся классом, и пакетом, классом не являющимся определяется использованием пакета. "Объявление класса" File:

package File;

В Perl нет специального ключевого слова для создания объекта. Однако, большинство ОО модулей на CPAN используют название метода new для для конструктора нового объекта:

my $hostname = File->new(
    path          => '/etc/hostname',
    content       => "foo\n",
    last_mod_time => 1304974868,
);

( Не беспокойтесь о том, что делает оператор ->, позже мы объясним.)

Blessing ("освящение")

Как ранее говорилось, большинство объектов в Perl являются хешами, но вообще объект может являться любым типом данных Perl (скаляр, массив и т.д.). Превращение простой структуры данных в объект выполняется через "освящение" этой структуры функцией bless.

Хотя мы настоятельно не рекомендуем вам собирать ваши объекты с нуля, вы должны знать термин bless. Объектом является "блесснутая" структура данных (также известная как "ссылка"). Иногда мы говорим, что объект "освящен в класс".

Для того, чтобы узнать название класса "освященного" объекта, используйте функцию blessed из модуля ядра Scalar::Util. Она возвращает название класса, если вызывается с объектом, или false в ином случае.

use Scalar::Util 'blessed';

print blessed($hash);      # undef
print blessed($hostname);  # File

Конструктор

Конструктор создает новый объект. Конструктор класса в Perl является обычным методом, в этом состоит отличие от других языков программирования, в которых есть отдельный синтаксис для конструктора. Большинство классов в Perl используют в качестве имени конструктора new:

my $file = File->new(...);

Методы

Как вы уже узнали, метод является обычной процедурой, выполняющей операции с объектом. Вы можете думать о методе, как о чём то, что объект может сделать. Если объект это существительное, то методы это глаголы (save, print, open).

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

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

$file->print_info;
# The file is at /etc/hostname

Особенность методов в том, как они вызываются. Использование (->) сообщает Perl, что вызывается метод.

При вызове метода Perl помещает в первом аргументе инвокант метода. Странное название инвокант обозначает то, что находится слева от стрелки. Инвокантом может быть имя класса или объект. Методу можно передать дополнительные аргументы:

sub print_info {
    my $self   = shift;
    my $prefix = shift // "This file is at ";

    print $prefix, ", ", $self->path, "\n";
}

$file->print_info("The file is located at ");
# The file is located at /etc/hostname

Атрибуты

В любом классе могут определяться собственные атрибуты. При создании объекта мы присваиваем значения этим атрибутам. Например, у любого объекта File имеется атрибут "путь файла". Атрибуты иногда называют свойствами.

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

Мы рекомендуем выполнять доступ к атрибутам только через аксессоры. Это методы, получающие или устанавливающие значение соответствующего атрибута. Ранее в примере с print_info мы видели вызов $self->path.

Вам могут также встретиться термины getter и setter. Это две разновидности аксессоров. Геттер возвращает значение атрибута, сеттер устанавливает. Другое название сеттера - mutator.

Обычно атрибуты определяются либо как только для чтения, либо для чтения и записи (read-only и read-write). Read-only атрибуты можно установить только при создании объекта, в то время, как read-write атрибуты можно изменять когда угодно.

Значением атрибута может являться объект. Например, вместо возврата последнего времени модификации в виде числа, класс С<File> может вернуть объект DateTime, представляющий это время.

Не все классы выставляют наружу изменяемые атрибуты. Также не все классы имеют атрибуты и методы.

Полиморфизм

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

Хотя два класса могут отличаться во многих аспектах, когда дело доходит до вызова print_content, они ведут себя одинаково. Это означает, что мы можем попытаться вызвать print_content() на объекте любого из этих классов, и мы не обязаны знать, какому классу принадлежит объект!

Полиморфизм одна из ключевых концепций объектно-ориентированной архитектуры.

Наследование

Наследование позволяет создавать специализированные версии существующих классов. Оно позволяет новым классам повторно использовать методы и атрибуты других классов. Например, можно создать класс File::MP3, наследующий от класса File. Все mp3-файлы являются файлами, но не все файлы являются mp3-файлами.

Мы часто называем отношение наследования, как отношение родитель-потомок или отношение суперкласс/подкласс.

File является суперклассом класса File::MP3, и File::MP3 является подклассом класса File.

package File::MP3;

use parent 'File';

Модуль parent предоставляет один из нескольких способов определить отношение наследования в Perl.

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

Обратите внимание, что не является ошибкой многократно определять подклассы любого класса. Это общепринято и обычно безопасно. Например, можно определить классы File::MP3::FixedBitrate и File::MP3::VariableBitrate для различных типов mp3 файлов.

Перегрузка и Разрешение Методов

Наследование позволяет разделять код между классами. По умолчанию, любой метод в родительском классе доступен в дочернем классе. Дочерний класс может явным образом перегрузить метод родительского класса своей реализацией. Например, объект класса File::MP3 содержит метод print_info() класса File:

my $cage = File::MP3->new(
    path          => 'mp3s/My-Body-Is-a-Cage.mp3',
    content       => $mp3_data,
    last_mod_time => 1304974868,
    title         => 'My Body Is a Cage',
);

$cage->print_info;
# The file is at mp3s/My-Body-Is-a-Cage.mp3

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

package File::MP3;

use parent 'File';

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
    print "Its title is ", $self->title, "\n";
}

$cage->print_info;
# The file is at mp3s/My-Body-Is-a-Cage.mp3
# Its title is My Body Is a Cage

Процесс определения того, какой метод нужно вызвать, называется разрешением вызова метода. Для этого Perl сначала рассматривает класс объекта (File::MP3 в данном случае). Если данный метод определен, вызывается версия метода этого класса. Если нет, Perl изучает каждый родительский класс поочередно. Класс File::MP3 имеет единственного родителя File. Если нужный метод не определен в File::MP3, но определен в File, Perl вызовет метод из File.

Если File наследует от DataSource, который наследует от Thing, Perl будет выполнять поиск "вверх по цепочке".

Есть возможность явно вызвать метод родителя из потомка:

package File::MP3;

use parent 'File';

sub print_info {
    my $self = shift;

    $self->SUPER::print_info();
    print "Its title is ", $self->title, "\n";
}

Часть SUPER:: говорит Perl искать print_info() в цепочке наследования классов File::MP3. Метод вызывается, когда найден родительский класс, реализующий этот метод.

Ранее мы рассказали о множественном наследовании. Его основная проблема в значительном усложнении разрешения вызова методов. Подробнее об этом написано в perlobj.

Инкапсуляция

Идея инкапсуляции в том, что объект непрозрачен. Когда другой разработчик использует ваш класс, ему не надо знать, как он реализован. Все, что ему надо знать, это что делает ваш класс.

Инкапсуляция важна по нескольким причинам. Во-первых, она позволяет вам отделить публичное API от закрытой реализации. Это значит, что вы можете менять реализацию без боязни сломать API.

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

Ранее мы упоминали, что большинство Perl объектов под капотом реализуется в виде хеша. Принципы инкапсуляции запрещают полагаться на это знание. Вместо этого, для доступа к данным хеша нужно использовать методы-аксессоры. Все объектные системы, рекомендуемые ниже, способны автоматически генерировать аксессоры. Никогда не потребуется выполнять доступ к объекту непосредственно через хеш, если вы используете одну из таких систем.

Композиция

В объектно-ориентированном коде часто можно видеть, что один объект ссылается на другой. Это называется композицией, или отношением has-a.

Ранее мы говорили, что аксессор last_mod_time класса File может возвращать объект DateTime. Это великолепный пример композиции. Можно пойти дальше и заставить аксессоры path и content также возвращать объекты. В этом случае класс File будет составлен из нескольких других объектов.

Роли

Роли описывают то, что класс делает, в отличии от того, чем он является. Хотя роли - относительно новая сущность в Perl, они стали довольно популярны. Роли применяются к классам. Иногда говорят, что классы могут потреблять роли.

Роли являются альтернативой наследованию для обеспечения полиморфизма. Предположим, у нас есть два класса, Radio и Computer. У радио и компьютера обычно есть переключатель вкл/выкл, и мы хотим смоделировать этот аспект в определении наших классов.

Можно унаследовать оба класса от общего родителя, например, Machine, но переключатели вкл/выкл имеются не у всех машин. Можно создать родительский класс HapOnOffSwitch, но такой подход выглядит очень искусственно. У радио и компьютера нет специализации по родителю, в реальности такой родитель выглядел бы довольно смешным созданием.

В этой ситуации на помощь приходят роли. Появляется смысл создать роль HasOnOffSwitch и применить ее к обеим классам. Роль должна определять известное API, например, методы turn_on() и turn_off().

В Perl нет никаких встроенных способов описать роль. В прошлом, скрипя зубами, люди просто использовали множественное наследование. В настоящее время на CPAN имеется несколько неплохих альтернатив для работы с ролями.

Когда стоит использовать ОО

Объектно-ориентированная парадигма не является наилучшим решением всех возможных проблем. Дамиан Конвей в своей книге Perl Best Practices (copyright 2004, Published by O'Reilly Media, Inc.) описывает несколько критериев, которые помогут вам понять, подходит ли ОО для решения ваших проблем:

  • Проектируемая вами система большая, или может превратиться в большую.

  • Данные можно объединить в очевидные структуры, особенно если данных в каждой структуре много.

  • Различные типы данных объединяются в естественную иерархию таким образом, что становится возможным использовать наследование и полиморфизм.

  • Есть данные, над которыми выполняется много различных действий.

  • На связанных типах данный надо выполнять одинаковые общие действия, но с с небольшими различиями, зависящими от типа данных.

  • Вы предполагаете, что потребуется добавлять новые типы данных в будущем.

  • Типичные взаимодействия между частями данных лучше всего представляются операторами.

  • Вероятно, что реализация отдельных компонентов системы может меняться со временем.

  • Система уже имеет объектно-ориентированную архитектуру.

  • Вашими модулями будут пользоваться многие другие программисты.

ОО СИСТЕМЫ НА PERL

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

Мы настоятельно рекомендуем использовать одну из таких систем. Даже самая минималистичная из них позволяет избежать множества рутинных действий. В самом деле, нет хороших причин писать ваши классы с нуля.

Если вам интересны внутренности, срытые всеми этими системами, смотрите perlobj.

Moose

Moose позиционирует себя, как "постмодернистская объектная система Perl5". Не пугайтесь слова "постмодернизм", это всего лишь отсылка к описанию Perl, данному Ларри: "первый постмодернистский компьютерный язык".

Moose предлагает полнофункциональную, современную ОО систему. Наибольшее влияние на нее оказала Common List Object System, но были также позаимствованы идеи из Smalltalk и некоторых других языков. Moose был создан Stevan Little и в значительной степени опирается на его работу по проектированию ОО архитектуры Perl6.

Вот определение класса File с использованием Moose:

package File;
use Moose;

has path          => ( is => 'ro' );
has content       => ( is => 'ro' );
has last_mod_time => ( is => 'ro' );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

Moose предлагает следующие возможности:

  • Декларативный сахар

    Для объявления классов Moose предоставляет прослойку из декларативного "сахара". Это просто набор экспортируемых функций, с которыми объявление того, как работает ваш класс, становится проще и приятней.

    Процедура has() объявляет атрибут, и Moose автоматически создает аксессор для него. Также он позаботится о создании метода new(). Этот конструктор знает об объявленных атрибутах и поэтому вы можете устанавливать их, когда создаете новый File.

  • Встроенные роли

    Moose позволяет определять роли таким же образом, как и классы:

    package HasOnOfSwitch;
    use Moose::Role;
    
    has is_on => (
        is  => 'rw',
        isa => 'Bool',
    );
    
    sub turn_on {
        my $self = shift;
        $self->is_on(1);
    }
    
    sub turn_off {
        my $self = shift;
        $self->is_on(0);
    }
  • Миниатюрная система типов

    В примере выше можно увидеть, как мы передаем isa => 'Bool' в has() при создании нашего атрибута is_on. Это говорит Moose, что значение этого атрибута должно иметь логический тип. При попытке установить неверное значение в этот атрибут наша программа сгенерирует исключение.

  • Полная интроспекция и манипуляция

    Встроенные возможности интроспекции Perl крайне минимальны. Moose построен поверх них и создает целый уровень интроспекции для ваших классов. Он позволит вам задавать вопросы типа "какие методы реализованы в классе?" Также он позволяет изменять классы программным путем.

  • Самодостаточность и расширяемость

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

  • Богатая экосистема

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

  • Множество других возможностей

    Moose - это очень мощный инструмент и здесь не получится рассказать о всех его возможностях. Мы подстрекаем вас узнать больше о Moose, начать можно с чтения Moose::Manual.

Конечно же, Moose не является совершенством.

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

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

Прежде чем начать паниковать имейте ввиду, что множество людей используют Moose при написании инструментов командной строки и другого кода, находящегося в зависимом положении от скорости запуска.

Moose также зависит от нескольких других модулей. Большинство из них небольшие, самодостаточные модули, некоторые из них отпочковались от самого Moose. Сам Moose и некоторые его зависимости требуют наличия компилятора. Если вам нужно устанавливать ваши программы на системах без компилятора, или наличие любых зависимостей является для вас проблемой, в этом случае Moose вам не подходит.

Moo

Если вы рассматривали Moose и решили, что одна из его проблем не позволяет вам его использовать, предлагаем взглянуть на Moo. Moo реализует подмножество функциональности Moose в обычном пакете. Для конечного пользователя API большинства реализуемых возможностей идентично Moose. Это означает, что вы легко можете переключиться с Moo на Moose.

Moo не реализует большинство API интроспекции Moose, поэтому часто загрузка ваших модулей происходит быстрей. В добавок, ни одна из его зависимостей не требует XS и он может быть установлен на машины без компилятора.

Одной из привлекательных особенностей Moo является его совместимость с Moose. Когда кто то попытается использовать API интроспекции из Moose в своих классах или ролях, эти классы и роли прозрачно раздуются до классов и ролей Moose. Эта способность облегчает включение кода, использующего Moo в базирующийся на Moose код, и наоборот.

Например, Moose класс может стать подклассом Moo класса с помощью extend, или использовать роль Moo с помощью with.

Авторы Moose надеются, что однажды Moo станет не нужным, когда Moose достаточно улучшится, но сейчас Moo является достойной альтернативой.

Class::Accessor

Class::Accessor является полярной противоположностью Moose. Он предоставляет очень немного возможностей и не является самодостаточным.

Однако, он очень прост, написан на читом Perl и зависит только от модулей ядра. Также он предлагает "Moose-подобный" API для поддерживаемых им возможностей, подключаемый по запросу.

Хотя даже он и делает не так много, предпочтительней писать ваши классы с его помощью, а не с нуля.

Вот наш класс File с использованием Class::Accessor:

package File;
use Class::Accessor 'antlers';

has path          => ( is => 'ro' );
has content       => ( is => 'ro' );
has last_mod_time => ( is => 'ro' );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

Флаг импорта antlers указывает Class::Accessor, что вы хотите определять атрибуты, используя Moose-подобный синтаксис. is единственный параметр, который можно передать в has. Если вы решите использовать Class::Accessor, рекомендуем вам пользоваться Moose-подобным синтаксисов, потому что будут легче переключиться на Moose, если возникнет такое желание.

Подобно Moose, Class::Accessor генерирует аксессоры и конструктор для вашего класса.

Class::Tiny

Наконец, у нас есть Class::Tiny. Этот модуль полностью соответствует своему имени. Он невероятно мал и не зависит абсолютно ни от чего, кроме Perl. Однако, мы все еще считаем, что много легче использовать его, чем писать ваш ОО код с нуля.

В очередной раз наш File:

package File;
use Class::Tiny qw( path content last_mod_time );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

Всё!

С Class::Tiny все аксессоры будут иметь тип read-write. Он также генерирует аксессоры и конструктор.

Для использования Moose-подобного синтаксиса есть Class::Tiny::Antlers.

Role::Tiny

Как описывалось выше, роли предоставляют альтернативу наследованию, но в Perl нет никакой встроенной поддержки ролей. Если ваш выбор пал на Moose, вы получите заодно полноценную реализация ролей. Однако, если вы выбрали один из других рекомендуемых ОО модулей, вам можете использовать роли из Role::Tiny.

Role::Tiny обеспечивает почти те же возможности, что и система ролей Moose, но в намного меньшем пакете. Более значимо, что он не поддерживает ни в каком виде объявление атрибутов, так что придется делать это вручную. Все же, он полезен, и хорошо работает с связке с Class::Accessor и Class::Tiny.

Резюме по ОО системам

Краткий повтор рассмотренных вариантов:

  • Moose

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

  • Class::Accessor

    Class::Acessor умеет намного меньше Moose, но является хорошей альтернативой, если вы находите Moose чересчур усложненным. Он существует давным-давно и хорошо протестирован в боевых условиях. Также имеет режим минимальной совместимости с Moose, облегчающий переход на Moose.

  • Class::Tiny

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

  • Role::Tiny

    Используйте Role::Tiny вместе с Class::Accessor или Class::Tiny, если вы начали рассматривать множественное наследование. Если вы дорастете до использования Moose, то в нем есть собственная реализация ролей.

Другие ОО системы

Кроме рассмотренных здесь, на CPAN можно найти буквально дюжины ОО модулей и вы, вероятно, запускали один или более из них, если работали с чужим кодом. Кроме того, в дикой природе чрезвычайно много кода, который выполняет всю работу ОО "своими руками". Если вам необходимо поддерживать такой код, читайте perlobj для понимания, как работает Perl ОО изнутри.

ЗАКЛЮЧЕНИЕ

Как говорилось, минималистичная система Perl ОО привела к появлению систем ОО на CPAN. Хотя вы все еще можете жить в каменном веке и кодировать ваши классы вручную, нет реальных причин делать так в современном Perl.

Для небольших систем об основных шаблонных действиях позаботятся Class::Tiny или Class::Accessor.

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

Мы предлагаем вам поиграться с Moose, Class::Accessor и Class::Tiny и сделать выбор, подходящий именно вам.

ПЕРЕВОДЧИКИ

  • Андрей Асякин <asan999 at gmail.com>

  • Николай Мишин <mi at ya.ru>