Программирование — управляющая деятельность. Чем больше программа, тем большим количеством деталей вам приходится управлять. Наша единственная надежда справиться с этой сложностью — использовать абстракцию (обращаться с похожими вещами похожим способом) и инкапсуляцию (групировать вместе связанные части).
Одних функций недостаточно для крупных задач. Существует несколько техник, группирующих функции в единицы связанного поведения. Одна из популярных техник — объектное ориентирование (ОО), или объектно-ориентированное программирование (ООП), где программы работают с объектами — обособленными, уникальными сущностями, обладающими собственной индивидуальностью.
Стандартная объектная система Perl 5 гибка, но минималистична. Вы можете построить замечательные вещи на её основе, но она даёт лишь небольшую помощь в некоторых базовых задачах. Moose — это полноценная объектная система для Perl5 (footnote: См. perldoc Moose::Manual
для подробной информации.). Она предоставляет более простой стандартный функционал и продвинутые возможности, заимствованные из таких языков как Smalltalk, Common Lisp и Perl 6. Код Moose работает совместно со стандартной объектной системой, и на данный момент является лучшим способом написания объектно-ориентированного кода в современном Perl 5.
Объект в Moose — это конкретный экземпляр класса, который представляет собой шаблон, описывающий данные и поведение, характерные для объекта. Классы используют пакеты (Пакеты) для создания пространств имён:
package Cat
{
use Moose;
}
Этот класс Cat
выглядит так, как будто ничего не делает, но это всё, что нужно Moose для создания класса. Объекты (или экземпляры) класса Cat
создаются с помощью следующего синтаксиса:
my $brad = Cat->new();
my $jack = Cat->new();
Как стрелка разыменовывает ссылку, также она и вызвает метод объекта или класса.
Метод — это функция, ассоциированная с классом. Как функции принадлежат пространствам имён, также и методы принадлежат классам, с двумя отличиями. Во-первых, метод всегда оперирует инвокантом. Вызов new()
на Cat
в сущности отправляет классу Cat
сообщение. Имя класса, Cat
, будет инвокантом new()
. Если вы вызываете метод на объекте, этот объект будет инвокантом:
my $choco = Cat->new();
$choco->sleep_on_keyboard();
Во-вторых, вызов метода всегда задействует стратегию диспетчеризации, когда объектная система выбирает соответствующий метод. Учитывая простоту Cat
, стратегия диспетчеризации очевидна, но существенная часть мощи ОО происходит из этой идеи.
Внутри метода первым аргументом будет инвокант. Идиоматический Perl 5 использует $self
как его имя. Предположим, что
package Cat
{
use Moose;
sub meow
{
my $self = shift;
say 'Meow!';
}
}
Теперь все экземпляры Cat
могут разбудить вас с утра из-за того, что их ещё не покормили:
my $fuzzy_alarm = Cat->new();
$fuzzy_alarm->meow() for 1 .. 3;
Методы, пользующиеся доступом к данным инвоканта — это методы экземпляра, потому что для корректной работы они требуют присутствия соответствующего инвоканта. Методы (такие как meow()
), не обращающиеся к данным экземпляра, — это методы класса. Вы можете вызывать методы класса на классах и методы классов и экземпляров на экземплярах, но нельзя вызывать методы экземпляра на классах.
Конструкторы, которые создают экземпляры, очевидно являются методами класса. Moose предоставляет вам конструктор по умолчанию.
Методы класса — это, в сущности, глобальные функции в пространстве имён. Без доступа к данным экземпляра они имеют немного преимуществ над функциями в пространстве имён. Большая часть ОО кода должным образом использует методы экземпляров, так как у них есть доступ к данным экземпляра.
Каждй объект в Perl 5 уникален. Объекты могут содержать приватные данные, ассоциированные с каждым уникальным объектом — это атрибуты, данные экземпляра, или состояние объекта. Определите атрибут, объявив его как часть класса:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
}
Это читается как «Объекты Cat
имеют атрибут name
. Он доступен только для чтения и является строкой.»
Moose предоставляет функцию has()
, которая объявляет атрибут. Первый аргумент, в данном случае 'name'
, это имя атрибута. Пара аргументов is => 'ro'
объявляет, что этот атрибут доступен только для чтения (r
ead o
nly), так что вы не сможете его изменять после того, как установили. Наконец, пара isa => 'Str'
объявляет, что значение этого атрибута может быть только строкой (str
ing).
Как результат использования has
Moose создает метод-аксессор с именем name()
и позволяет вам передавать параметр name
в конструктор класса Cat
:
for my $name (qw( Tuxie Petunia Daisy ))
{
my $cat = Cat->new( name => $name );
say "Created a cat for ", $cat->name();
}
Документация Moose использует круглые скобки для разделения имён атрибутов и их свойств:
has 'name' => ( is => 'ro', isa => 'Str' );
Это равнозначно следующему:
has( 'name', 'is', 'ro', 'isa', 'Str' );
Подход Moose хорошо подходит для сложных объявлений:
has 'name' => ( is => 'ro', isa => 'Str', # дополнительные параметры Moose; perldoc Moose init_arg => undef, lazy_build => 1, );
…тогда как в этой книге предпочтение отдано подходу с меньшим количеством пунктуации для простых объявлений. Выберите пунктуацию, которая даёт вам наибольшую понятность.
Moose пожалуется, если вы передадите что-нибудь, не являющееся строкой. Атрибуты не обязаны иметь тип. В таком случае подойдёт что угодно:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'age', is => 'ro';
}
my $invalid = Cat->new( name => 'bizarre',
age => 'purple' );
Указание типа позволяет Moose выполнить для вас некоторую валидацию данных. Иногда подобная строгость бесценна.
Если вы пометите атрибут как доступный для чтения и записи (с помощью is => rw
), Moose создаст метод-мутатор, который может изменять значение этого атрибута:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'age', is => 'ro', isa => 'Int';
has 'diet', is => 'rw';
}
my $fat = Cat->new( name => 'Fatty',
age => 8,
diet => 'Sea Treats' );
say $fat->name(), ' eats ', $fat->diet();
$fat->diet( 'Low Sodium Kitty Lo Mein' );
say $fat->name(), ' now eats ', $fat->diet();
ro
-аксессор, используемый как мутатор, выбросит исключение Cannot assign a value to a read-only accessor at ...
.
Использование ro
или rw
— вопрос дизайна, удобства и чистоты. Moose не принуждает к какой-то определённой философии в этой области. Некоторые предлагают делать все данные экземпляра ro
, чтобы необходимо было передавать данные экземпляра в конструктор (Неизменяемость). В примере с Cat
age()
всё ещё может быть аксессором, но конструктор может принять год рождения кота и вычислить возраст сам, исходя из текущего года. Этот подход усиливает код валидации и гарантирует, что все создаваемые объекты будут иметь валидные данные.
Данные экземпляра начинают демонстрировать значение объектного-ориентированного подхода. Объект содержит связанные данные и может выполнять действия с этими данными. Класс только описывает эти данные и действия.
Moose позволяет вам объявить, какими атрибутами обладают экземпляры класса (у кота есть имя), а также атрибуты этих атрибутов (вы не можете изменить имя кота; вы можете его только читать). Moose сам решает, как хранить эти атрибуты. Вы можете изменить это, если захотите, но позволение Moose управлять вашим хранилищем поддерживает инкапсуляцию: скрытие внутренних деталей объекта от внешних пользователей этого объекта.
Рассмотрим изменение того, как Cat
управляет своим возрастом. Вместо передачи значения возраста в конструктор, передадим год рождения кота и вычислим возраст при необходимости:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
has 'birth_year', is => 'ro', isa => 'Int';
sub age
{
my $self = shift;
my $year = (localtime)[5] + 1900;
return $year - $self->birth_year();
}
}
Хотя синтаксис создания объектов Cat
изменился, синтаксис использования объектов Cat
остался прежним. Снаружи Cat
age()
ведёт себя так же, как и раньше. Как это работает внутренне — это забота класса Cat
.
Сохраните поддержку старого синтаксиса создания объектов Cat
, модифицировав генерируемый конструктор Cat
, чтобы позволить передавать параметр age
. На основе него вычислите birth_year
. См. perldoc Moose::Manual::Attributes
.
Вычисление возраста имеет другое преимущество. Значение атрибута по умолчанию будет вести себя правильно в случае, если кто-нибудь создат новый объект Cat
, не передав год рождения:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw', isa => 'Str';
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };
}
Ключевое слово default
у атрибута принимает ссылку на функцию (footnote: Вы можете использовать простое значение, такое как число или строка, напрямую, но для чего-либо более сложного используйте ссылку на функцию.), которая возвращает значение по умолчанию для этого атрибута при создании нового объекта. Если код, создающий объект, не передаст в конструктор значение для этого атрибута, объект получит значение по умолчанию:
my $kitten = Cat->new( name => 'Choco' );
…и этот котёнок будет иметь возраст 0
до следующего года.
Инкапсуляция полезна, но настоящие возможности объектного-ориентированного подхода намного шире. Хорошо спроектированная ОО-программа может управлять множеством типов данных. Когда хорошо спроектированные классы инкапсулируют конкретные детали объектов в соответствующих местах, происходит нечто любопытное: код зачастую становится менее конкретным.
Перемещение деталей того, что программа знает об отдельных объектах Cat
(атрибуты), и того, что программа знает о том, что объекты Cat
могут делать (методы) в класс Cat
означает, что код, работающий с экземплярами Cat
, может успешно игнорировать то, как Cat
делает то, что он делает.
Рассмотрим функцию, выводяющую детали объекта:
sub show_vital_stats
{
my $object = shift;
say 'My name is ', $object->name();
say 'I am ', $object->age();
say 'I eat ', $object->diet();
}
Очевидно (из контекста), что эта функцию работает, если вы передадите ей объект Cat
. Фактически, она будет делать то, что нужно, для любого объекта, имеющего три соответствующих аксессора, независимо от того, как этот объект предоставляет эти аксессоры, и независимо от того, что это за объект: Cat
, Caterpillar
или Catbird
. Функция имеет достаточно общий вид, чтобы любой объект, обеспечивающий этот интерфейс, был корректным параметром.
Это свойство полиморфизма означает, что вы можете заменить объект одного класса объектом другого класса, если они предоставляют один и тот же внешний интерфейс.
Некоторые языки и среды требуют установления формального отношения между двумя классами, прежде чем позволить программе заменять экземпляры одного экземплярами другого. Perl 5 предоставляет способы сделать эти проверки обязательными, но не требует их. Его стандартная ad-hoc-система позволяет вам обращаться с двумя любыми экземплярами, имеющими одинаково названные методы, как с вполне эквивалентными. Некоторые называют это утиной типизацией, утверждая, что любой объект, который может крякать (quack()
), достаточно похож на утку, чтобы с ним можно было обращаться как с уткой.
show_vital_stats()
заботится о том, чтобы инвокант был валиден, только в смысле поддержки этих трёх методов, name()
, age()
и diet()
, каждый из которых не имеет аргументов и возвращает нечто, что можно конкатенировать в строковом контексте. Вы можете иметь в своём коде сотню разных классов, не имеющих каких-либо очевидных взаимосвязей, но они будут работать с этим методом, если соответствуют этому ожидаемому поведению.
Подумайте, как вы могли бы обработать целый зоопарк животных без этой полиморфной функции? Выгода универсальности должна быть очевидна. Также, любые специфичные детали того, как рассчитать возраст оцелота или осьминога, могут принадлежать соответствующему классу — где они имеют наибольшее значение.
Конечно, одно лишь существование методов с именами name()
или age()
само по себе не определяет поведение объекта. Для объекта Dog
age()
может быть аксессором, с помощью которого вы можете узнать, что $rodney
— 9 лет, а $lucky
— 4 года. Объект Cheese
может иметь метод age()
, который позволяет вам контролировать, как долго хранить $cheddar
чтобы его вкус заострился. age()
может быть аксессором в одном классе, но не в другом:
# сколько лет коту?
my $years = $zeppie->age();
# хранить сыр на складе шесть месяцев
$cheese->age();
Иногда полезно знать, что делает объект, и что это означает.
Роль — это именованная совокупность поведений и состояний (footnote: Смотрите дизайн-документы Perl 6 на тему ролей по адресу http://feather.perl6.nl/syn/S14.html и исследуйте трейты Smalltalk на странице http://scg.unibe.ch/research/traits для получения глубоких подробностей.). Тогда как класс организует поведения и состояние в шаблон для создания объектов, роль организует именованную коллекцию поведений и состояния. Вы можете создать экземпляр класса, но не роли. Роль — это что-то, что делает класс.
Если у нас есть Animal
, имеющий возраст, и Cheese
, имеющий возраст, разница между ними может быть в том, что Animal
выполняет роль LivingBeing
, тогда как Cheese
выполняет роль Storable
:
package LivingBeing
{
use Moose::Role;
requires qw( name age diet );
}
Всё, что имеет эту роль, должно предоставлять методы name()
, age()
и diet()
. Класс Cat
может явно обозначить, что он имеет эту роль:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw', isa => 'Str';
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };
with 'LivingBeing';
sub age { ... }
}
Строчка with
заставляет Moose скомпоновать роль LivingBeing
в класс Cat
. Компоновка гарантирует, что все атрибуты и методы роли являются частью класса. LivingBeing
требует от любого компонуемого класса предоставлять методы с названиями name()
, age()
и diet()
. Cat
удовлетворяет этим ограничениям. Если LivingBeing
была бы скомпонована в класс, который не предоставляет эти методы, Moose бы выбросил исключение.
Ключевое слово with
, используемое для применение роли к классу, должно находиться после объявлений атрибутов, чтобы компоновка могла распознать все сгенерированные методы-аксессоры.
Теперь все экземпляры Cat
будут возвращать истинное значение, если их запросить, предоставляют ли они роль LivingBeing
. А объекты Cheese
— не будут:
say 'Alive!' if $fluffy->DOES('LivingBeing');
say 'Moldy!' if $cheese->DOES('LivingBeing');
Эта техника проектирования отделяет возможности классов и объектов от реализации этих классов и объектов. Поведение вычисления года рождения класса Cat
само по себе тоже может быть ролью:
package CalculateAge::From::BirthYear
{
use Moose::Role;
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };
sub age
{
my $self = shift;
my $year = (localtime)[5] + 1900;
return $year - $self->birth_year();
}
}
Выделение этой роли из Cat
делает полезное поведение доступным для других классов. Теперь Cat
может компоновать обе роли:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
with 'LivingBeing',
'CalculateAge::From::BirthYear';
}
Обратите внимание, как метод age()
из CalculateAge::From::BirthYear
удовлетворяет требование роли LivingBeing
. Также заметьте, что любая провера того, что Cat
выполняет роль LivingBeing
, возвращает истинное значение. Выделение age()
в роль изменило только детали того, как Cat
вычисляет возраст. Это всё ещё LivingBeing
. Cat
может выбрать, имплементировать ли свой собственный возраст или получить его откуда-то ещё. Всё что имеет значение — это то, что он предоставляет age()
, который удовлетворяет ограничениям LivingBeing
.
Как полиморфизм означает, что вы можете работать с разными объектами, имеющими одно и то же поведение, одним и тем же способом, так и этот алломорфизм означает, что объект может реализовывать одно и то же поведение разными способами.
Глубокий алломорфизм может уменьшить размер ваших классов и увеличить количество кода, разделяемого между ними. Он также позволяет вам именовать специфичные и обособленные поведения — очень полезно для проверки возможностей вместо реализаций.
Для сравнения ролей с другими техниками проектирования, такими как примеси, множественное наследование и манкипатчинг (monkeypatching) см. http://www.modernperlbooks.com/mt/2009/04/the-why-of-perl-roles.html.
Если вы компонуете роль в класс, класс и его экземпляры будут возвращать истинное значение если вы вызовете на них DOES()
:
say 'This Cat is alive!'
if $kitten->DOES( 'LivingBeing' );
Объектная система Perl 5 поддерживает наследование, которое устанавливает связи между двумя классами, так, что один уточняет другой. Дочерний класс ведёт себя так же, как его родитель — он имеет то же самое количество и типы атрибутов, и может использовать те же самые методы. Он может иметь дополнительные данные и поведение, но вы можете использовать любой экземпляр дочернего класса там, где код ожидает его родителя. В некотором смысле, подкласс предоставляет роль, предполагаемую существованием его родительского класса.
Должны ли вы использовать роли или наследование? Роли предоставляют безопасность времени компоновки, лучшую проверку типов, лучшее разбиение кода и более тонкий контроль над именами и поведениями, но наследование более привычно опытным разработчикам на других языках. Используйте наследование в случае, если один класс на самом деле расширяет другой. Используйте роль, если класс требует дополнительного поведения, и если вы можете дать этому поведению осмысленное имя.
Рассмотрите класс LightSource
, который предоставляет два публичных атрибута (enabled
и candle_power
) и два метода (light
и extinguish
):
package LightSource
{
use Moose;
has 'candle_power', is => 'ro',
isa => 'Int',
default => 1;
has 'enabled', is => 'ro',
isa => 'Bool',
default => 0,
writer => '_set_enabled';
sub light
{
my $self = shift;
$self->_set_enabled(1);
}
sub extinguish
{
my $self = shift;
$self->_set_enabled(0);
}
}
(Заметьте, что опция writer
атрибута enabled
создаёт приватный аксессор, используемый в классе для установки значения.)
Подкласс LightSource
может определить супер-свечу, дающую в сто раз больше света:
package SuperCandle
{
use Moose;
extends 'LightSource';
has '+candle_power', default => 100;
}
extends
принимает список имён классов для использования их в качестве родителей текущего класса. Если бы это была единственная строка в этом классе, объекты SuperCandle
вели бы себя в точности так же, как объекты LightSource
. Они имели бы оба атрибута, candle_power
и enabled
, так же как и методы light()
и extinguish()
.
+
в начале имени атрибута (как у candle_power
) указывает, что текущий класс делает с этим атрибутом что-то особенное. Здесь супер-свеча переопределяет значение по умолчанию источника света, так что любой новый созданный объект SuperCandle
будет иметь световое значение в 100 свечей.
Когда вы вызываете light()
или extinguish()
на объекте SuperCandle
, Perl будет искать методы в классе SuperCandle
, а затем в каждом из его родителей. В данном случае, эти методы находятся в классе LightSource
.
Наследование атрибутов работает аналогично (см. perldoc Class::MOP
).
Порядок диспетчеризации методов (или порядок разрешения методов, method resolution order, или MRO) очевиден для классов, имеющих одного родителя. Искать в классе объекта, затем в его родителе, и так далее, до тех пор, пока метод не будет найден или родители не закончатся. Классы, наследующие от нескольких родителей (множественное наследование) — Hovercraft
расширяет и Boat
, и Car
— требуют более хитрой диспетчеризации. Суждение о множественном наследовании сложно. По возможности избегайте множественного наследования.
Perl 5 использует стратегию разрешения методов поиском в глубину. Он ищет класс родителя, названного первым, и всех родителей этого родителя рекурсивно, прежде чем искать в классах следующих родителей. Прагма mro
(Прагмы) предоставляет альтернативные стратегии, включая стратегию C3 MRO, которая ищет в непосредственных родителях заданного класса, прежде чем искать в их родителях.
См. perldoc mro
для дальнейших деталей.
Как и с атрибутами, подклассы могут переопределять методы. Представьте свет, который вы не можете потушить:
package Glowstick
{
use Moose;
extends 'LightSource';
sub extinguish {}
}
Вызов extinguish()
на светящейся палочке не будет делать ничего, несмотря на то, что метод из LightSource
что-то делает. Диспетчеризация методов найдёт метод подкласса. Вы, возможно, не имели намерения так делать. Если имели, используйте функцию Moose override
чтобы ясно выразить своё намерение.
Внутри переопределённого метода функция Moose super()
позволит вам вызвать переопределённый метод:
package LightSource::Cranky
{
use Carp 'carp';
use Moose;
extends 'LightSource';
override light => sub
{
my $self = shift;
carp "Can't light a lit light source!"
if $self->enabled;
super();
};
override extinguish => sub
{
my $self = shift;
carp "Can't extinguish unlit light source!"
unless $self->enabled;
super();
};
}
Этот подкласс добавляет предупреждение при попытке зажечь или потушить источник света, который уже находится в нужном состоянии. Функция super()
отправляет к реализации текущего метода в ближайшем родителе, согласно стандартному порядку разрешения методов Perl 5.
Модификаторы методов Moose могут делать похожие вещи — и больше. Смотрите perldoc Moose::Manual::MethodModifiers
.
Метод isa()
в Perl возвращает истину, если его инвокант является названным классом или расширяет его. Этот инвокант может быть именем класса или экземпляром объекта.
say 'Looks like a LightSource'
if $sconce->isa( 'LightSource' );
say 'Hominidae do not glow'
unless $chimpy->isa( 'LightSource' );
Moose предоставляет много возможностей помимо стандартного ООП Perl 5. Хотя вы можете построить всё, что вы получаете с Moose, сами (Благословлённые ссылки), или смастерить это из нескольких CPAN-дистрибутивов, Moose стоит использования. Это логически связанное целое, с хорошей документацией. Многие важные проекты успешно его используют. Он имеет зрелое и внимательное сообщество разработчиков.
Moose позаботится о конструкторах, деструкторах, аксессорах и инкапсуляции. Вам придётся объявить, чего вы хотите, но то, что вы получите, будет безопасным и простым в использовании. Объекты Moose могут расширять и работать с объектами стандартной системы Perl 5.
Moose также даёт возможность метапрограммирования — манипулирования вашими объектами через сам Moose. Если вы когда-нибудь интересовались, какие методы доступны в классе или объекте, или какие атрибуты объект поддерживает, эта информация доступна:
my $metaclass = Monkey::Pants->meta();
say 'Monkey::Pants instances have the attributes:';
say $_->name for $metaclass->get_all_attributes;
say 'Monkey::Pants instances support the methods:';
say $_->fully_qualified_name
for $metaclass->get_all_methods;
Вы даже можете увидеть, какие классы расширяют заданный класс:
my $metaclass = Monkey->meta();
say 'Monkey is the superclass of:';
say $_ for $metaclass->subclasses;
Смотрите perldoc Class::MOP::Class
для большей информации об операциях с метаклассами и perldoc Class::MOP
для информации о метапрограммировании Moose.
Moose и его протокол метаобъектов (meta-object protocol, или MOP) даёт возможность лучшего синтаксиса для объявления и работы с классами и объектами в Perl 5. Это корректный код на Perl 5:
use MooseX::Declare;
role LivingBeing { requires qw( name age diet ) }
role CalculateAge::From::BirthYear
{
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };
method age
{
return (localtime)[5] + 1900
- $self->birth_year();
}
}
class Cat with LivingBeing
with CalculateAge::From::BirthYear
{
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
}
CPAN-дистрибутив MooseX::Declare
использует Devel::Declare
для добавления нового Moose-специфичного синтаксиса. Ключевые слова class, role и method уменьшают количество текста, необходимого для написания хорошего объектно-ориентированного кода на Perl 5. Обратите особое внимание на декларативную природу этого примера, а также на отсутствие my $self = shift;
в age()
.
Хотя Moose и не является частью ядра Perl 5, его популярность гарантирует его доступность в дистрибутивах многих ОС. Дистрибутивы Perl 5, такие как Strawberry Perl и ActivePerl, тоже его включают. Несмотря на то, что Moose — CPAN-модуль, а не базовая библиотека, его чистота и простота делают его неотъемлемой частью современного Perl-программирования.
Moose — не маленькая библиотека, но мощная. CPAN-модуль Any::Moose
помогает уменьшить стоимость возможностей, которые вы не используете.
Базовая система объектов Perl 5 сознательно минималистична. Она включает всего три правила:
Основываясь на этих трёх правилах, вы можете построить что угодно, но это всё, что вы получаете по умолчанию. Этот минимализм для больших проектов может быть непрактичен — особенно неловки и ограниченны возможности увеличения абстракции посредством метапрограммирования (Кодогенерация). Moose (Moose) — более подходящий выбор для современных программ, размер которых больше чем несколько сотен строк кода, хотя большое количество унаследованного кода всё ещё использует стандартное ООП Perl 5.
Последняя часть стандартного ООП Perl 5 — благословлённая ссылка. Встроенная функция bless ассоциирует имя класса со ссылкой. Эта ссылка становится корректным инвокантом, и Perl будет выполнять на ней диспетчеризацию методов, используя ассоциированный класс.
Конструктор — это метод, создающий и благословляющий ссылку. По соглашению конструкторы имеют имя new()
, но это не обязательно. Кроме того, конструкторы почти всегда являются методами класса.
bless
принимает два аргумента, ссылку и имя класса. Она возвращает ссылку. Ссылка может быть пустой. Класс ещё не обязательно должен существовать. Вы даже можете использовать bless
за пределами конструктора или класса (хотя все программы, кроме самых простых, должны использовать настоящие конструкторы). Канонический конструктор выглядит так:
sub new
{
my $class = shift;
bless {}, $class;
}
По замыслу, этот конструктор получает имя класса как инвокант метода. Но вы можете и захардкодить имя класса, потеряв при этом в гибкости. Параметрический конструктор даёт возможность повторного использования посредством наследования, делегирования или экспорта.
Используемый тип ссылки имеет значение только для того, как объект хранит свои данные экземпляра. Он не оказывает никакого другого влияния на результирующий объект. Ссылки на хеши наиболее распространены, но благословить можно любой тип ссылки:
my $array_obj = bless [], $class;
my $scalar_obj = bless \$scalar, $class;
my $sub_obj = bless \&some_sub, $class;
Классы Moose определяют атрибуты объекта декларативно, но стандартное ООП Perl 5 недостаточно формально. Класс, представляющий игроков в баскетбол, и хранящий номер футболки и позицию, может использовать такой конструктор:
package Player
{
sub new
{
my ($class, %attrs) = @_;
bless \%attrs, $class;
}
}
…и создавать игроков так:
my $joel = Player->new( number => 10,
position => 'center' );
my $dante = Player->new( number => 33,
position => 'forward' );
Методы класса могут обращаться к атрибутам объекта напрямую как к элементам хеша:
sub format
{
my $self = shift;
return '#' . $self->{number}
. ' plays ' . $self->{position};
}
…но так же это может делать и любой другой код, так что любое изменение внутреннего представления объекта может сломать другой код. Методы-аксессоры безопаснее:
sub number { return shift->{number} }
sub position { return shift->{position} }
…и вот вы уже начинаете вручную писать то, что Moose даёт вам бесплатно. Мало того, Moose поощряет использовать аксессоры вместо прямого доступа, скрывая код генерации аксессоров. Прощайте, соблазны.
При наличии благословлённой ссылки, вызов метода осуществляется следующим образом:
my $number = $joel->number();
…ищет имя класса, ассоциированного с благословлённой ссылкой $joel
— в данном случае Player
. Затем Perl ищет функцию (footnote: Вспомните, что Perl 5 не делает различия между функциями в пространстве имён и методами.) с именем number()
в классе Player
. Если такой функции не существует и если Player
расширяет другой класс, Perl ищет в родительском классе (и так далее и так далее), до тех пор, пока не найдёт number()
. Если Perl находит number()
, он вызывает этот метод с $joel
в качестве инвоканта.
CPAN-модуль namespace::autoclean
может помочь избежать непреднамеренных коллизий между импортируемыми функциями и методами.
Moose предоставляет extends
для отслеживания связей наследования, Perl 5 же использует глобальную переменную пакета с именем @ISA
. Диспетчер методов ищет в @ISA
каждого класса имена его родительских классов. Если InjuredPlayer
расширяет Player
, вы можете написать так:
package InjuredPlayer
{
@InjuredPlayer::ISA = 'Player';
}
Прагма parent
(Прагмы) чище (footnote: Старый код может использовать прагму base
, но parent
заменила base
в Perl 5.10.):
package InjuredPlayer
{
use parent 'Player';
}
Moose имеет собственную метамодель, сохраняющую расширенную информацию о наследовании; это обеспечивает дополнительные возможности.
Вы можете наследовать от нескольких родительских классов:
package InjuredPlayer;
{
use parent qw( Player Hospital::Patient );
}
…хотя есть предостережения против множественного наследования и сложности диспетчеризации методов. Рассмотрите использование в качестве замены ролей (Роли) или модификаторов методов Moose.
Если в классе инвоканта или в любом из его суперклассов нет подходящего метода, дальше Perl 5 будет искать функцию AUTOLOAD()
(AUTOLOAD) в каждом классе в соответствии с выбранным порядком разрешения методов. Perl вызовет любую найденную функцию AUTOLOAD()
, чтобы она предоставила или отклонила желаемый метод.
AUTOLOAD()
делает множественное наследование гораздо более сложным для понимания.
Как и в Moose, в стандартном ООП Perl 5 вы можете перегружать методы. В отличие от Moose, стандартный Perl 5 не предоставляет механизмов для указания вашего намерения переопределить родительский метод. Хуже того, любая функция, которую вы предварительно определили, определили или импортировали в дочерний класс, может перегрузить метод родительского класса, если имеет то же самое имя. Даже если вы забыли использовать предоставляемую Moose систему перегрузки с override
, она по крайней мере существует. Стандартное ООП Perl 5 не предлагает такой защиты.
Чтобы перегрузить метод в дочернем классе, объявите метод с тем же именем, что и метод в родительском классе. Внутри перегруженного метода можно вызвать родительский метод с помощь подсказки диспетчера SUPER::
:
sub overridden
{
my $self = shift;
warn 'Called overridden() in child!';
return $self->SUPER::overridden( @_ );
}
Префикс SUPER::
у имени метода указывает диспетчеру методов перейти к перегруженному методу с соответствующим именем. Вы можете указать собственные аргументы для перегруженного метода, но большая часть кода использует @_
. Не забудьте удалить вызывающую сущность с помощью shift
, если вы так делаете.
SUPER::
имеет сбивающий с толку недостаток: он диспетчеризует к родителю пакета, в который был скомпилирован перегруженный метод. Если вы импортировали этот метод из другого пакета, Perl с удовольствием направит к неверному родителю. Потребность в обратной совместимости привела к сохранению этой неудачной функции. Модуль SUPER
из CPAN предлагает обходной путь. В Moose super()
не страдает от такого недостатка.
Если благословлённые ссылки выглядят минималистичными, коварными и сбивающими с толку, такие они и есть. Moose — огромное улучшение. Используйте его везде, где возможно. Если же вам придётся поддерживать код, использующий благословлённые ссылки, или вы ещё не смогли убедить свою команду полностью перейти на Moose, вы можете обойти некоторые проблемы благословлённых ссылок с помощью дисциплины.
Class::Accessor
, чтобы избежать повторяющегося кода.AUTOLOAD()
. Если вам необходимо его использовать, используйте предварительные объявления ваших функций (Объявление функций), чтобы помочь Perl понять, какая функция AUTOLOAD
предоставляет реализацию метода.bless
, и разбивая ваши классы на наименьшие возможные самостоятельные единицы кода.
Рефлексия (или интроспекция) — это процесс получения от программы информации о ней самой во время её выполнения. Работая с кодом как с данными, вы можете управлять кодом так же, как вы управляете данными. На этом принципе основана кодогенерация (Кодогенерация).
Class::MOP
из Moose (Class::MOP) упрощает многие задачи рефлексии для объектных систем. Если вы используете Moose, его система метапрограммирования поможет вам. Если нет, несколько других идиом стандартного Perl 5 помогут вам инспектировать и манипулировать запущенными программами.
Если вы знаете имя модуля, вы можете проверить, считает ли Perl, что этот модуль загружен, заглянув в хеш %INC
. Когда Perl 5 загружает код с помощью use
или require
, он сохраняет запись в %INC
, где ключ — файловый путь загружаемого модуля, а значение — полный путь к модулю на диске. Другими словами, загрузка Modern::Perl
фактически делает следующее:
$INC{'Modern/Perl.pm'} =
'.../lib/site_perl/5.12.1/Modern/Perl.pm';
Точный путь будет различаться в зависимости от вашей инсталляции. Чтобы проверить, что Perl успешно загрузил модуль, преобразуйте имя модуля в каноническую файловую форму и проверьте существование этого ключа в %INC
:
sub module_loaded
{
(my $modname = shift) =~ s!::!/!g;
return exists $INC{ $modname . '.pm' };
}
Как и с @INC
, любой код в любом месте может манипулировать %INC
. Некоторые модули (такие как Test::MockObject
или Test::MockModule
) манипулируют %INC
с благими намерениями. В зависимости от уровня вашей паранойи, вы можете сами проверять путь и ожидаемое содержимое пакета.
Функция is_class_loaded()
CPAN-модуля Class::Load
инкапсулирует эту проверку %INC
.
Чтобы проверить существование пакета в вашей программе — того, что какой-то код где-то выполнил директиву package
с заданным именем — проверьте, что пакет наследует от UNIVERSAL
. Всё, что расширяет UNIVERSAL
, должно так или иначе предоставлять метод can()
. Если такого пакета не существует, Perl выбросит исключение о некорректной вызывающей сущности, так что оберните этот вызов в блок eval
:
say "$pkg exists" if eval { $pkg->can( 'can' ) };
Альтернативный подход — копание в таблице символов Perl.
Так как Perl 5 не делает строгих различий между пакетами и классами, лучшее, что вы можете сделать без Moose — проверить существование пакета ожидаемого имени класса. Вы можете проверить с помощью can()
, что этот пакет предоставляет new()
, но нет никаких гарантий, что найденный new()
будет методом или конструктором.
Модули не обязаны предоставлять номера версий, но каждый пакет наследует метод VERSION()
от универсального родительского класса UNIVERSAL
(Пакет UNIVERSAL):
my $mod_ver = $module->VERSION();
VERSION()
возвращает номер версии заданного модуля, если он определён. В противном случае он возвращает undef
. Если модуль не существует, метод тоже вернёт undef
.
Чтобы проверить, существует ли функция в пакете, вызовите can()
как метод класса на имени пакета:
say "$func() exists" if $pkg->can( $func );
Perl выбросит исключение, если $pkg
не является допустимым инвокантом; оберните вызов метода в блок eval
если у вас есть какие-либо сомнения в его валидности. Имейте ввиду, что функция, реализованная в терминах AUTOLOAD()
(AUTOLOAD), может давать неправильный ответ, если в пакете функции нет корректного предварительного объявления функции или корректной перегрузки can()
. Это ошибка в другом пакете.
Используйте эту технику для определения того, импортировал ли import()
модуля функцию в текущее пространство имён:
say "$func() imported!" if __PACKAGE__->can( $func );
Как и в случае проверки существования пакета, вы можете сами покопаться в таблице символов, если у вас есть для этого достаточно терпения.
Нет надёжного способа для рефлексии определить разницу между функцией и методом.
Таблица символов Perl 5 — это специальный тип хеша, где ключи — это имена глобальных символов пакета, а значения — тайпглобы (typeglob). Тайпглоб — это внутренняя структура данных, которая может содержать любой из или все типы — скаляр, массив, хеш, дескриптор файла и функцию.
Обратиться к символьной таблице как к хешу можно добавив двойное двоеточие к имени пакета. Например, символьная таблица пакета MonkeyGrinder
доступна как %MonkeyGrinder::
.
Вы можете проверить существование конкретных имён символов в таблице символов с помощью оператора exists
(или манипулировать таблицей символов для добавления или удаления символов, если хотите). Однако имейте ввиду, что определённые изменения в ядре Perl 5 модифицировали детали того, что хранят тайпглобы, когда и как.
Загляните в секцию «Symbol Tables» в perldoc perlmod
для получения подробностей, а затем отдайте предпочтение другим техникам рефлексии, описанным в этом разделе. Если вам действительно нужно манипулировать таблицами символов и тайпглобами, рассмотрите использование вместо этого CPAN-модуля Package::Stash
.
Создание и использование объектов в Perl 5 с Moose (Moose) просто. Проектирование хороших программ — нет. Вы должны выдерживать баланс между проектированием слишком много и слишком мало. Только практический опыт может помочь вам понять наиболее важные техники проектирования, но несколько принципов могут указать вам направление.
Новички в ОО-проектировании зачастую чрезмерно применяют наследование для повторного использования кода и применения полиморфизма. Результат этого — глубокая иерархия классов с ответственностями, разбросанными по неверным местам. Поддержка этого кода тяжела — кто знает, где добавить или отредактировать поведение? Что случится, если код в одном месте конфликтует с кодом, объявленным где-то ещё?
Наследование — лишь один из многих инструментов. Car
может расширять Vehicle::Wheeled
(отношение is-a, является), но лучше, если Car
будет содержать несколько объектов Wheel
в качестве атрибутов экземпляра (отношение has-a, имеет).
Разбиение сложных классов на меньшие, специализированные сущности (будь то классы или роли), улучшает инкапсуляцию и уменьшает возможность того, что какой-либо класс или роль слишком разрастётся. Меньшие, более простые и лучше инкапсулированные сущности проще для понимания и поддержки.
Когда вы проектируете вашу объектную систему, обдумайте ответственности каждой сущности. Например, объект Employee
может представлять индивидуальную информацию об имени человека, контактную информацию и другие личные данные, а объект Job
может представлять рабочие обязанности. Разделение этих сущностей по их ответственностям позволяет классу Employee
принимать во внимание только проблему управления информацией, касающейся того, кто человек есть, а классу Job
— представлять, что человек делает. (Например, два сотрудника Employee
могут иметь договорённость по разделению одной работы Job
.)
Если каждый класс имеет единственную ответственность, вы улучшаете инкапсуляцию свойственных классу данных и поведения и уменьшаете связанность между классами.
Сложность и дублирование затрудняют разработку и поддержку. Принцип DRY (Don't Repeat Yourself, Не повторяйтесь) — напоминание о необходимости находить и устранять дублирование внутри системы. Дублирование существует в данных, так же, как и в коде. Вместо повторения конфигурационной информации, пользовательских данных и других артефактов внутри вашей системы, создаёте единое, каноническое представление этой информации, из которого вы сможете генерировать другие артефакты.
Этот принцип помогает уменьшить вероятность того, что важные части вашей системы могут потерять синхронизированность, а также помогает найти оптимальное представление системы и её данных.
Принцип замещения Лисков предполагает, что вы должны иметь возможность поставить специализацию класса или роли на место оригинала, не нарушив API оригинала. Другими словами, объект должен быть настолько же или более общим в отношении того, что он ожидает, и как минимум так же конкретен в отношении того, что он производит.
Представьте два класса, Dessert
и его дочерний класс PecanPie
. Если классы следуют принципу замещения Лисков, вы можете заменить в наборе тестов каждое использование объектов Dessert
объектами PecanPie
, и все тесты должны пройти (footnote: См. «IS-STRICTLY-EQUIVALENT-TO-link Рэга Брейтуейта (Reg Braithwaite) для дальнейших подробностей, http://weblog.raganwald.com/2008/04/is-strictly-equivalent-to.html.).
Moose позволяет вам объявить и использовать типы, а также расширять их с помощью подтипов, для формирования ещё более специализированного описания того, что представляют собой ваши данные и как они себя ведут. Эти аннотации типов помогают проверить, что данные, с которыми вы собираетесь работать в конкретных функциях и методах, соответствующи, и даже указать механизмы преобразования данных одного типа в данные другого типа.
См. Moose::Util::TypeConstraints
и MooseX::Types
для дальнейшей информации.
Новички в ООП зачастую обращаются с объектами так, как будто они пачка записей, использующих методы для получения и установки внутренних значений. Эта простая техника ведёт к неприятному соблазну распространить обязанности объекта по всей системе.
В случае хорошо спроектированного объекта, вы говорите ему, что делать, но не как это делать. Как правило большого пальца, если обнаружили, что обращаетесь к данным экземпляра объекта (даже с помощью методов-аксессоров), у вас, возможно, слишком много доступа к внутренностям объекта.
Один из подходов, предотвращающих такое поведение — считать объект неизменяемым. Передайте нужные данные в их конструкторы, а затем запретите любые модификации этой информации из-за пределов класса. Не делайте видимыми методы изменения данных экземпляра. Сконструированные таким образом объекты всегда корректны с момента своего создания и не могут стать некорректными в результате внешних манипуляций. Для достижения этого требуется огромная дисциплина, но получающиеся в результате системы надёжны, легко тестируются и поддерживаются.
Некоторые варианты проектирования заходят так далеко, что запрещают изменение данных экземпляра внутри самого класса, хотя этого достичь гораздо труднее.