пятница, 26 декабря 2008 г.

"Закрытые объекты" в Perl5 и Perl6

Введение

Как вы знаете на геральдическом гербе Perl красуется надпись TMTOWTDI (There's More Than One Way To Do It - есть более одного способа сделать это). Но чтобы использовать один из способов, необходимо обладать свободой - свободой выбора. Идея свободы выбора насквозь пронизывает Perl. Кстати, как и христианство.

Используя Perl, вы полностью свободны! Свободны и в том, насколько быть свободным и открытым.

Обычно и вас не ограничивают. Просто там, куда не следует лезть без особой на то нужды, вывешивают табличку с предупреждающей надписью. Некоторые таблички являются табличками по умолчанию и продиктованы здравым смыслом. Конечно вам никто не запрещает проигнорировать предупреждение, но тогда только вы сами будете отвечать за все последствия. Вспомните, что произошло с Адамом и Евой, когда они съели то злополучное яблоко.

Но соблазн велик. К тому-же нарушитель попытается повесить всех собак на других. Как попытался это сделать в свое время Адам, сказав: "Это все Ева, а ведь ТЫ дал мне ее в жены". Так вот, чтобы такие Адамы не лезли куда ни попадя, можно ограничить свободу при использовании вашего модуля.

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

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

А как же Perl5?! Perl - гибкий язык, поэтому можно и в Perl5 предпринять кое-что для защиты объектов. Одним из самых популярных способов сделать это является "выворачивание объекта наизнанку" :-).


Inside-out объекты

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

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

package Foo;

sub new {
my $self = { name => "My name" };
bless $self;
return $self;
}

sub name {
my $self = shift;
return $$self{name};
}

1;

Можно получить значения foo в обход одноименного метода:

my $foo = Foo->new();
print $foo->{name};

Чтобы предотвратить это, необходимо запрятать от внешнего взора значение foo. Вопрос только как и где?! Например, в лексической области пакета. То есть объект перестает быть совокупностью данных с привязанными к нему процедурами, он становиться некоторым идентификатором с привязанными к нему процедурами, которые в свою очередь по этому идентификатору знают о местоположении данных.

Такие объекты называются Inside-out объектами. Кстати, в стандартной поставе Perl 5.10 имеется модуль Hash::Util::FieldHash, предназначенный для создания таких объектов.

Рассмотрим пример Inside-out объекта:

package Foo;

use Scalar::Util 'refaddr';
my %name;
sub new {
my $self = bless \do {my $anon_scalar}, "Foo";
$name{ refaddr $self } = "My name";
return $self;
}

sub name {
my $self = shift;
return $name{ refaddr $self };
}

sub DESTROY {
my $self = shift;
delete $name{ refaddr $self };
}

1;

Этот объект функционально полностью идентичен первому, и работа с ним осуществляется как с обычными объектами:

my $foo = Foo->new();
print $foo->name();

А вот получить значение name в обход метода доступа невозможно. Строка print $foo->{name}; приведет к ошибке: "Not a HASH reference".

Чтобы облегчить создание Inside-out объектов и, не дай Бог, не забыть о DESTROY существует множество модулей-конструкторов, среди которых особое место занимают Class::InsideOut и Object::InsideOut. Объекты, созданные при помощи их, можно клонировать, сериализовать и
использовать под mod_perl.

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

При всех своих преимуществах Inside-out объекты не лишены недостатков. А именно, при их использовании возникают определенные трудности с наследованием.

Вообще-то наследование и закрытость - это взаимоисключающие вещи. И приходиться чем-то жертвовать.

Например, объекты, созданные при помощи Class::InsideOut могут наследовать другие, но от них наследоваться невозможно. А при использовании Object::InsideOut, наследование реализовано как делегирование.

Маленький хак

Закрытость - закрытостью, но насколько это закрытость сильна?!

Попробуем добраться до приватных атрибутов Inside-out объекта, внедрив в модуль объекта маленькую подпрограмму-шпиона по имени hook:

use Scalar::Util 'refaddr';

# Внедряем метод доступа.
sub Foo::hook {
my $self = shift;
return $name{ refaddr $self };
}

my $foo = Foo->new();
print $foo->hook();

И, вуаля, все тайное становиться явным!

О Perl6

Если внимательно посмотреть на вышеприведенный хак, то можно увидеть что-то очень знакомое... Да это же аналог роли из Perl6!

Роль добавляет к объектам как методы, так и атрибуты. Добавляет либо на этапе компиляции, либо на этапе исполнения.

По сути роли, кроме всего прочего, открывают то, что cкрыто за приватными методами и атрибутами!

А когда роль добавляется на этапе исполнения, это привносит в class-based объекты черты prototype-based. Если пойти еще дальше: то там, где есть роли, в классическом наследовании вообще нет необходимости.

Но prototype-based стиль не приемлет закрытости. С другой стороны, как говорит, Stevan Little, один из основных разработчиков Moose, нужда в private методах и атрибутах - это социальная проблема и должна решаться именованием, документированием и хорошей поркой.

Подводя итоги

В perl можно сделать закрытые объекты. Но такие объекты в первую очередь предназначены для защиты от случайных изменений.

Если нужно, то "закрытой" объект можно "открыть". Кончено, кроме объекта, построенного на замыкании, но такой объект является вещью в себе - он не может ни наследовать, ни наследоваться.

Perl в воем развитии продолжает движение навстречу людям, не знакомым с ним. Но Perl6 остается perl, он лишь принимает формы привычные многим.