воскресенье, 18 августа 2013 г.

Написание серьёзных приложений на Perl. Безусловный минимум, который вам необходимо знать

Перевод статьи: Writing serious Perl. The absolute minimum you need to know
Автор: Хеннинг Кох

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

Оглавление
Пространства имён

Один пакет никогда не должен пересекаться с пространством имён другого пакета, до тех пор, пока тот явно не попросит об этом. Поэтому, никогда не определяйте методы в другом скрипте для использования с require. Всегда оборачивайте вашу библиотеку в пакет и используйте его. Таким образом пространство имён останется чистым и отделённым:
package Sophie;
sub say_hello {
  print "Привет, Мир!";
}

package Clara;
use Sophie;          # Загрузить пакет, но НЕ импортировать какие-либо методы
say_hello();         # Не сработает
Sophie->say_hello(); # Правильное использование
Sophie::say_hello(); # Работает, кроме наследуемых методов

Корень пространства имён

Если вы используете выгруженный пакет Some::Package, Perl ищет файл Some/Package.pm в текущем каталоге. Если этот файл не существует, поиск продолжается в другом корне пространства имён (например, в c:/perl/lib) из глобального массива @INC.

Хорошая мысль сохранить пакеты вашего приложения в отдельный каталог, например в lib, и добавлять этот каталог к списку корней пространства имён с помощью use lib 'my/root/path':
use lib 'lib';     # Добавить подкаталог 'lib' к корневому пространству имён @INC
use Some::Package; # Пройтись по @INC в поисках файла пакета
Экспорт идентификаторов

Бывают случаи, когда нужно экспортировать методы или имен переменных в вызывающий пакет. Я делаю так лишь в редких случаях, когда очень-очень часто требуются статические вспомогательные методы. Для экспорта идентификаторов наследуйте от класса Exporter и заполните массив @EXPORT идентификаторов, которые хотите экспортировать:
package Util;
use base 'Exporter';
our @EXPORT = ('foo', 'bar');

sub foo {
  print "foo!";
}
sub bar {
  print "bar!";
}

package Amy;
use Util; # Импортировать символы в @EXPORT
foo();    # Работает
bar();    # Работает
Постарайтесь не засорять пространство имён другого пакета, если у вас нет для этого достаточно уважительной причины! Однако, как бы то ни было, большинство пакетов на CPAN экспортируют идентификаторы при их явном подключении.

Будет неплохо, если оставить программам возможность самим решать, какие идентификаторы нужно экспортировать в их пространство имён. Для этого можно воспользоваться массивом @EXPORT_OK или @EXPORT.
package Util;
use base 'Exporter';
our @EXPORT_OK = ('foo', 'bar');

sub foo {
  print "foo!";
}
sub bar {
  print "bar!";
}

package Amy;
use Util 'foo'; # Импортировать только foo()
foo();          # Работает
bar();          # Не сработает
Быстрое приготовление структур данных

Используйте { } для создания ссылок на анонимные хэши. Используйте [ ] для создания ссылок на анонимные массивы. Сочетайте эти конструкции для создания более сложных структур данных вроде списков из хэшей:
my @students = ( { name         => 'Clara',
                   registration => 10405,
                   grades       => [ 2, 3, 2 ] },
                 { name         => 'Amy',
                   registration => 47200,
                   grades       => [ 1, 3, 1 ] },
                 { name         => 'Deborah',
                   registration => 12022,
                   grades       => [ 4, 4, 4 ] } );
Используйте -> для получения доступа к значениям структуры данных:
# Напечатать имена всех студентов
foreach my $student (@students) {
  print $student->{name} . "\n";
}

# Напечатать вторую оценку Клары
print $students[0]->{grades}->[1];

# Удалить код регистрации Клары
delete $students[0]->{registration};
Классы и объекты

Пакеты - это классы. Объекты - это обычно ссылки на хэши, "благословлённые"* именем класса. Атрибуты - это пары ключ/значение в хэше.

Конструкторы

Конструкторы - это статические методы, которым довелась участь возвращать объекты:
package Student;

sub new {
  my($class, $name) = @_;       # Первый параметр - название класса
  my $self = { name => $name }; # Ссылка на анонимный хэш, содержащий атрибуты экземпляра
  bless($self, $class);         # Говорит: $self - это $class
  return $self;
}

package main;
use Student;
my $amy = Student->new('Amy');
print $amy->{name};             # Доступ к атрибуту
Вместо Student->new('Amy') можно также написать Student('Amy'). Однако отметим, что в этом случае Perl полагается на непредсказуемую эвристику, чтобы понять ваши истинные намерения и иногда может ошибаться.

Множественные конструкторы

Поскольку ключевое слово new не имеет особого значения в Perl, можно создавать столько методов-конструкторов, сколько захочется и называть их как захочется. Например, вам может потребоваться два разных конструктора, в зависимости от того, каким образом нужно сформировать объект - загрузив существующую запись из базы данных или создав новый экземпляр с нуля:
my $amy = Student->existing('Amy');
my $clara = Student->create();
Поскольку конструктор явным образом возвращает конструируемый объект, $self не является чем-то необычным. Например, вы можете взять $self из статического кэша уже сконструированных объектов:
package Coke;
my %CACHE;

sub new {
  my($class, $type) = @_;
  return $CACHE{$type} if $CACHE{$type}; # По возможности использовать копию из кэша
  my $self = $class->from_db($type);     # Получить его из базы данных
  $CACHE{$type} = $self;                 # Помещаем в кэш полученный объект
  return $self;
}

sub from_db {
  my($class, $type) = @_;
  my $self = ...        # Получить данные из базы данных
  bless($self, $class); # Сделать $self экземпляром $class
  return $self;
}

package main;
use Coke;

my $foo = Coke->new('Lemon');   # Получение из базы данных
my $bar = Coke->new('Vanilla'); # Получение из базы данных
my $baz = Coke->new('Lemon');   # Используется копия из кэша
Ради полноты картины я должен напомнить, что ссылки в %CACHE будут удерживать кэшированные объекты в памяти, даже если остальные экземпляры объектов перестали существовать. Поэтому, если ваши кэшированные объекты обладают методами-деструкторами, они не будут вызваны до момента завершения программы.

Методы экземпляров

Методы экземпляров получают ссылку на вызываемый объект в первом параметре:
package Student;

sub work {
  my($self) = @_;
  print "$self работает\n";
}
sub sleep {
  my($self) = @_;
  print "$self спит\n";
}

package main;
use Student;

my $amy = Student->new('Amy');
$amy->work();
$amy->sleep();
Ссылка на себя (this в Java) никогда не подразумевается в Perl:
sub work {
  my($self) = @_;
  sleep();        # Не делайте этого!
  $self->sleep(); # Правильное использование
}
Статические методы

Статические методы получают имя вызывающего класса в первом параметре. Конструкторы - это просто статические методы:
package Student;

sub new {
  my($class, $name) = @_;
  # ...
}
sub list_all {
  my($class) = @_;
  # ...
}

package main;
use Student;
Student->list_all();
Методы экземпляров могут вызывать статические методы с помощью$self->static_method():
sub work {
  my($self) = @_;
  $self->list_all();
}
Наследование

Наследование осуществляется при помощи use base 'Base::Class':
package Student::Busy;
use base 'Student';

sub night_shift {
  my($self) = @_;
  $self->work();
}

sub sleep { # Перекрывает метод родительского класса
  my($self) = @_;
  $self->night_shift();
}
Все классы автоматически наследуют некую основополагающую функциональность, такую как isa или can, от класса UNIVERSAL. Также, если вы чувствуете навязчивое желание выстрелить себе в ногу с помощью множественного наследования, Perl не станет вас останавливать.

Строгие атрибуты экземпляров

Поскольку традиционный объект является просто ссылкой на хэш, вы можете воспользоваться любым именем атрибута и Perl не будет жаловаться:
use Student;
my $amy = Student->new('Amy');
$amy->{gobbledegook} = 'какое-то значение'; # Работает
Часто хочется задать список допустимых атрибутов так, чтобы Perl завершался с ошибкой, если кто-то попытался воспользоваться неизвестным атрибутом. Это можно сделать при помощи прагмы fields:
package Student;
use fields 'name',
           'registration',
           'grades';

sub new {
  my($class, $name) = @_;
  $self = fields::new($class); # Возвращает пустой "строгий" объект
  $self->{name} = $name; # Получаем доступ к атрибуту как обычно
  return $self; # $self уже благословлён
}

package main;
use Student;

my $clara = Student->new('Clara');
$clara->{name} = 'WonderClara'; # Работает
$clara->{gobbledegook} = 'foo'; # Не работает
Памятка о принципе обобщённого доступа

Кто-то может воротить нос от того, каким образом я получаю доступ к атрибутам экземпляров в моих примерах. Писать $clara->{name} легко до тех пор, пока нужно только лишь получить сохранённое значение. Однако, каким образом нужно изменить пакет Student, если при доступе к атрибуту {name}, потребуется произвести какие-то вычисления (вроде сцепления полей {first_name} и {last_name}), что нужно сделать? Замена публичного интерфейс пакета и замена всех упоминаний $clara->{name} на $clara->get_name() не является приемлемым решением.

По существу, остаётся лишь одна из двух возможностей:
  • Можно воспользоваться tie** для того, чтобы при обращении к скаляру $clara->{name} класса выполнить необходимые вычисления атрибута. Я считаю этот процесс утомительным в чистом Perl, однако вам стоит заглянуть на страницу perltie документации Perl и составить собственное впечатление.
  • Использовать только методы доступа (также известные как геттеры и сеттеры) и сделать незаконным прямое обращение к атрибуту в вашем программном проекте. Лично я предпочитаю этот способ, потому что он позволяет сделать код красивее и предоставляет больше контроля над видимостью атрибутов для других классов. CPAN содержит различные модули, автоматизирующие создание методов доступа. Я хочу показать, как развернуть собственный генератор методов доступа в разделе Расширение языка.
Импорт

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

Параметры импорта

Вы можете передать параметры любому пакету, который вы используете:
package Student;
use Some::Package 'param1', 'param2';
Каждый раз при использовании пакета все параметры, с которыми вы его используете, передаются статическому методу import из этого пакета:
package Some::Package;
sub import {
  my($class, @params) = @_;
}
Кто вызвал?

Функция caller() позволяет вам (среди всего прочего) находить, какой класс вызвал текущий метод:
package Some::Package;

sub import {
  my($class, @params) = @_;
  print "Гляди, " . caller() . " пытается меня импортировать!";
}
Расширение языка

Давайте соединим наши знания и напишем простые члены пакета, которые настраивают поля для вызывающего пакета и создают удобные методы доступа к этим полям:
package members;

sub import {
  my($class, @fields) = @_;
  return unless @fields;
  my $caller = caller();
  
  # Построим код, который вычислим для вызывающего
  # Выполним вызов fields для вызывающего пакета
  my $eval = "package $caller;\n" .
             "use fields qw( " . join(' ', @fields) . ");\n";

  # Сгенерируем удобные методы доступа
  foreach my $field (@fields) {
    $eval .= "sub $field : lvalue { \$_[0]->{$field} }\n";
  }

  # Вычислим подготовленный код
  eval $eval;

  # $@ содержит возможные ошибки вычисления
  $@ and die "Ошибка настройки членов для $caller: $@";
}

# И ещё немного кода ниже...
package Student;
use members 'name',
            'registration',
            'grades';

sub new {
  my($class, $name) = @_;
  $self = fields::new($class);
  $self->{name} = $name;
  return $self;
}

package main;
my $eliza = Student->new('Eliza');
print $eliza->name;           # Гляди, мам, фигурных скобок нет! То же, что и $eliza->name()
$eliza->name = 'WonderEliza'; # Работает, потому что наш метод доступа является lvalue
print $eliza->name;           # Напечатает "WonderEliza"
Ценные ресурсы
Заключительные слова

Я рад, если это небольшое руководство оказалось для вас полезным. Если у вас есть вопросы или комментарии, задавайте их (только не отправляйте мне ваши домашние задания).

По теме заметки: я написал прагму под названием Reformed Perl - исправленный Perl, которая облегчает многие задачи ООП в Perl 5 и предоставляет намного более приятный синтаксис. Посмотрите сами!

Об авторе

Хеннинг Кох - студент Аугсбургского университета информатики и мультимедиа, Германия. Он ведёт блог о проектировании и технологиях программного обеспечения по адресу http://www.netalive.org/swsu/.

Примечания переводчика

* В Perl объект создаётся на основе ссылки операцией bless, что переводится как "благословлять".

** Операция tie - это, в некотором роде, родственник операции bless. Переводится как "связывать", превращает объект в структуру данных, с которой можно обращаться точно так же, как с хэшем, массивом или файловым дескриптором. Подробнее об этом можно почитать, например, тут: Изменение поведения хэша с использованием функции tie.

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

Комментариев нет: