Объектно-ориентированное программирование в Perl 5

7 апреля 2011

Когда создавался Perl, ООП еще не был моден. Сейчас он моден, в связи с чем некоторые граждане, не обнаружив в этом языке любимых классов, испытывают культурный шок, плюются и идут учить модные PHP и Python. Тем не менее, если вы используете в своей работе Perl и хотите программировать в ООП стиле, язык не будет стоять у вас на пути. Единственная проблема, с которой вам предстоит столкнутся — это проблема выбора, потому что в Perl, как обычно, «there’s more than one way to do it».

Примечание: Для тех, кто не знает Perl, но хотел бы его освоить, в этом блоге есть соответствующая серия уроков.

1. Традиционные модули (еще не ООП)

Чуть более 99% скриптов на Perl — это небольшие утилиты в пару сотен строк кода. Но что делать в тех редких случаях, когда требуется написать большую программу, разбив ее код на несколько файлов? Традиционное решение, не имеющее отношения к ООП, выглядит следующим образом. Все функции выносятся в отдельные модули (файлы с расширением .pm). Основной же скрипт (файл .pl) подгружает эти модули, а те в свою очередь подгружают модули, от которых зависят сами. Скрипт берет функции, объявленные в подгруженных модулях, и с их помощью делает свое темное дело.

Рассмотрим пример модуля (файл MyLib.pm):

#!/usr/bin/perl

package MyLib;
use strict;

sub Test {
  print "MyLib - Test Ok!\n";
}

1; # сообщает интерпретатору, что все ОК

… а также пример скрипта, использующего этот модуль:

#!/usr/bin/perl

use strict;
use MyLib;

MyLib::Test();

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

Файл MyLib.pm:

#!/usr/bin/perl

package MyLib;
use strict;
use Exporter 'import';

our @EXPORT_OK = qw/Test/;

sub Test {
  print "MyLib - Test Ok!\n";
}

1;

Файл test.pl:

#!/usr/bin/perl

use strict;
use MyLib qw/Test/;

Test(); # вызовет MyLib::Test()

Подробности о модуле Exporter можно найти в CPAN’е.

2. Добавляем объектно-ориентированность

В Perl 5 есть три типа данных — скаляры, массивы и хэши. Как же получить с их помощью объекты? Оказывается, все очень просто. Берем специальную функцию bless(), передаем ей первым аргументом указатель на переменную (как правило, используются хэши) и название класса вторым аргументом. То, что было передано первым аргументом, становится объектом! Вот как это выглядит на практике.

Файл MyCalss.pm:

#!/usr/bin/perl

package MyClass; {
  # ^ фигурные скобки - для красоты
  # а в Perl >= 5.14 можно без точки с запятой
  use strict;

  sub new {
    # получаем имя класса
    my($class) = @_;
    # создаем хэш, содержащий свойства объекта
    my $self = {
      name => 'MyClass',
      version => '1.0',
    };
    # хэш превращается, превращается хэш...
    bless $self, $class;
    # ... в элегантный объект!

    # эта строчка - просто для ясности кода
    # bless и так возвращает свой первый аргумент
    return $self;
  }

  # метод get_name();
  sub get_name {
    my($self) = @_; # ссылка на объект
    return $self->{name}; # достаем имя из хэша
  }
}

1; # ok!

Пример скрипта, использующего MyClass:

#!/usr/bin/perl

use strict;
use MyClass;

# создаем новый объект
# в конструктор можно было передать дополнительные аргументы
# которые шли бы в sub new() следом за именем класса

# my $cl = new MyClass(); # олдскульный стиль
my $cl = MyClass->new();

# доступ к имени можно получить напрямую
print "Name:               ".$cl->{name}."\n";
# но правильнее делать это через гетер
# аналогично вызову MyClass::get_name($cl, другие-аргументы);
print "get_name() returns: ".$cl->get_name()."\n";

А вот — пример класса-наследника:

#!/usr/bin/perl

package MyClassChild; {
  use base MyClass; # родительский класс
  use strict;

  # переопределяем конструктор
  sub new {
    my($class) = @_;
    my $self = MyClass::new($class);
    $self->{name} = "MyClassChild";
    return $self;
  }

  # тут можно объявить дополнительные методы
}
1;

Тут все здорово и замечательно, но, к сожалению, нет инкапсуляции. Вернее, если я не ошибаюсь, это называется «инкапсуляция по соглашению». Это когда мы даем protected методам имена, начинающиеся, например, с одного подчеркивания, а private методам — имена, начинающиеся с двух подчеркиваний и говорим, что первые должны вызываться только из класса и его потомков, а вторые — только из класса. При желании можно даже написать небольшой скрипт, делающий проверку, что инкапсуляция нигде не нарушается. Я где-то слышал, что такой тип инкапсуляции вполне успешно используется в некоторых языках (SmallTalk ?).

Дополнение: Собственно, такой подход — это все, что вам нужно, если класс не имеет свойств (то есть вы просто экспортируете функции в ООП стиле). Если свойства есть, вы можете ограничить доступ к ним с помощью модулей Attribute::Constant, Scalar::Readonly и других.

3. Inside-out классы

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

Пример inside-out класса:

#!/usr/bin/perl

package MyClass; {
  use strict;
  use Scalar::Util qw/refaddr/;

  # $items{refaddr $obj}{name} = value
  my %items;

  sub new {
    my($class, %args) = @_;
    # сам объект представляет собой пустой хэш
    my $self = bless {}, $class;

    my $id = refaddr $self;
    $items{$id}{name} = $args{name} || 'Anonymous';
    $items{$id}{version} = '1.0';

    return $self;
  }

  sub get_name {
    my($self) = @_;
    return $items{refaddr $self}{name};
  }

  sub DESTROY {
    my($self) = @_;
    # print $items{refaddr $self}{name}." destroyed!\n";
    delete $items{refaddr $self};
  }
}

1; # ok!

Пример класса-наследника не привожу, потому что там все тривиально. Скрипт, использующий наш класс:

#!/usr/bin/perl

use strict;
use MyClass;

my $anonymous = MyClass->new();
my $alexandr = MyClass->new(
  name => "Alexandr"
);

print "anonymous->get_name() = ".$anonymous->get_name()."\n";
print "alexandr->get_name()  = ".$alexandr->get_name()."\n";

Программист, который будет использовать класс, не может получить доступ к хэшу %MyClass::items, потому что он объявлен в модуле MyClass как «my», а не «our». Поскольку все свойства класса хранятся в этом хеше, они автоматически становятся закрытыми. При желании мы даже можем объявить закрытые методы.

Однако у этого подхода есть МНОГО недостатков. Во-первых, мы не можем получить protected методы. Впрочем, некоторые программисты заверяют, что при правильном проектировании они не очень то и нужны. Во-вторых, чтобы избежать утечки памяти, мы должны явно освобождать ее в деструкторе. В-третьих, серьезные проблемы возникают в многопоточных приложениях. И, наконец, стандартные средства сериализации не работают с inside-out объектами.

По названным причинам inside-out классы редко объявляются описанным выше образом. Обычно для этого используются специальные CPAN-модули.

4. Модуль Class::InsideOut и иже с ним

Одним из таких модулей является Class::InsideOut. Пример класса, созданного с его помощью:

#!/usr/bin/perl

package MyClass; {
  use strict;
  use Class::InsideOut qw/:std/;

  # для объявления public-свойств
  # public ___ => my %____;

  # свойство, открытое только для чтения
  readonly name => my %name;

  # закрытое свойство
  private version => my %version;

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

    register($self); # для многопоточности

    $name{id $self} = $args{name} || 'Anonymous';
    return $self;
  }
}

1; # ok!

Пример дочернего класса:

#!/usr/bin/perl

package MyClassChild; {
  use base MyClass;
  use Class::InsideOut qw/:std/;
  use strict;

  # опять же, тут никаких protected членов

  readonly second_name => my %second_name;

  sub new {
    my ($class, %args) = @_;
    my $self = MyClass::new($class, %args);
    $second_name{id $self} = $args{second_name} || 'Noname';
    return $self;
  }

  sub private_test : PRIVATE {
    # ...
  }
}

1;

Пример скрипта, использующего MyClassChild:

#!/usr/bin/perl

use strict;
use MyClassChild;
use Scalar::Util qw/blessed/;

my $anonymous = MyClassChild->new();
my $alexandr = MyClassChild->new(
  name => "Alexandr",
  second_name => "Alexeev"
  # blog => http://eax.me/
);

print "anonymous->name() = ".$anonymous->name()."\n";
print "alexandr->name()  = ".$alexandr->name()."\n";

# пример RTTI в Perl
if(blessed($alexandr) eq "MyClassChild") {
  print "alexandr->second_name()  = ".
    $alexandr->second_name()."\n";
}

Идея та же, что и в предыдущем пункте, только каждому свойству соответствует отдельный хэш. На этот раз не нужно освобождать память в деструкторе или беспокоиться о многопоточности — Class::InsideOut сделает все за нас.

Из недостатков модуля хотелось бы отметить отсутствие «традиционных» сетеров/геттеров, имена которых начинались бы с set_ и get_, а также нерешенную проблему с сериализацией. В описании модуля сказано, что данный функционал находится в разработке. Учитывая, что модуль последний раз обновлялся в 2008 году, вряд ли мы этого функционала дождемся.

Среди аналогичных CPAN-модулей следует отметить Object::InsideOut и Class::Std. Первый отпугнул меня объемом документации, но выглядит неплохо. Второй поддерживает «правильные» сетеры и гетеры, а также сериализацию, но в нем не реализована поддержка многопоточности.

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

5. Великий и могучий Moose

В описании Moose говориться следующее:

Основная цель Moose заключается в том, чтобы сделать ООП в Perl 5 легче, более устойчивым и менее утомительным. С Moose вы можете думать больше о том, что вы хотите сделать, и меньше о механике ООП.

Пример использования Moose (иногда еще говорят «классов в стиле Modern Perl»):

#!/usr/bin/perl

package MyClass; {
  use strict;
  use Moose;
  use MooseX::Privacy;

  # объявляем новое свойство
  # строка, только для чтения, по умолчанию - "Anonymous"
  has 'name' => (
    is => 'ro',
    isa => 'Str',
    default => 'Anonymous',
  );

  # аналогично, но это свойство - protected
  has 'version' => (
    is => 'ro',
    isa => 'Str',
    default => '1.0',
    traits => [qw/Protected/], # или Private
  );

  # или protected_method
  private_method print_info => sub {
    my($self, $format, $params) = @_;

    printf $format."\n", $self->{name};

    if(scalar keys %{$params}) {
      print "Params:\n";
      for my $k(keys %{$params}) {
        print "  $k => $params->{$k}\n";
      }
    }
  }; # нужна точка с запятой!

  sub BUILD {
    my($self, $params) = @_;
    $self->print_info(
      'MyClass with name %s created!',
      $params
    );
  }; # тут точка с запятой вообще-то не нужна,
     # но чтобы гарантированно не накосячить,
     # лучше ставить ее везде
}

1; # ok!

Пример класса-наследника:

#!/usr/bin/perl

package MyClassChild; {
  use strict;
  use Moose;
  extends 'MyClass';

  # новое свойство
  has 'second_name' => (
    is => 'ro',
    isa => 'Str',
    default => 'Anonymous',
  );
 
  # изменяем унаследованное свойство
  has '+version' => (
    default => '1.1',
  );

  sub BUILD {
    my($self, $args) = @_;
    print "Child's version is $self->{version}\n";
  }
}

1;

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

В Moose реализовано все возможности, предлагаемые большинством современных ООП языков. Модуль снабжен прекрасной документацией (см Moose::Cookbook). Также был обнаружен очень интересный модуль MooseX::Declare, добавляющий немного синтаксического сахара в Moose. К сожалению, на момент написания этих строк, он был несовместим с MooseX::Privacy.

6. Модуль Perl6::Classes

И последний модуль, рассматриваемый в этой заметке — Perl6::Classes. Он представляет собой попытку перенести в Perl 5 возможности и синтаксис Perl 6, связанные с ООП.

#!/usr/bin/perl

use strict;
use Perl6::Classes;

class Human {
  has $.name; # все свойства - закрытые; это - хорошая практика

  method BUILD {
    $.name = shift;
  }

  method get_name {
    return $.name;
  }

  method test_protected is protected {
  }

  method test_private is private {
  }

  # Разница между method и sumbethod в том, что последний
  # не наследуется потомками. Если сделать этот метод
  # наследуемым, в дочерних классах он будет вызван дважды.
  submethod DESTRUCT {
    print "Destroying $.name\n";
  }
}

# проверка наследования
class Developer is Human {
  method test {
    $self->test_protected();
#    $self->test_private();
  }

  submethod DESTRUCT {
    print "Developer's destructor...\n";
  }
}

# проверка полиморфизма
# ( я с трудом могу представить ситуацию, где в Perl
# требовались бы _виртуальные_ методы )
sub polymorph_test {
  my ($obj) = @_;
  return $obj->get_name();
}

my $human = Human->new("Alex");
my $dev = Developer->new("Afiskon");
$dev->test();
print "Developer's name: ".polymorph_test($dev)."\n";
print "Human's name: ".polymorph_test($human)."\n";

Этот модуль прекрасно иллюстрирует расширяемость синтаксиса Perl, но, к сожалению, не применим на практике из-за наличия нескольких серьезных багов (см описание в CPAN). Судя по тому, что модуль не обновляется с 2003-го года, работа над ним остановлена.

7. Так что же выбрать?

Если принять во внимание недостатки inside-out классов и модуля Perl6::Classes, то оказывается, что у нас всего два варианта. Использовать либо традиционные blessed hashes, либо Moose. Как обычно, использовать следует то, что больше подходит для данной задачи. Например, свой небольшой класс имеет смысл писать на «чистом Perl», а при использовании фреймворка Catalyst лучше воспользоваться Moose. Все равно Catalyst, начиная с версии 5.8, использует этот модуль.

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.