воскресенье, 24 ноября 2013 г.

Шаблоны проектирования в Perl, часть 3

Перевод статьи: Perl Design Patterns, Part 3
Автор: Фил Кроу (Phil Crow)

Эта третья (и заключительная) статья в серии статей, составляющих единый ответ программистов Perl на книгу "Шаблоны проектирования" (также известную как "книга банды четырёх", поскольку её написали четверо авторов). Как было показано во второй статье, Perl предоставляет типы, необходимые для реализации многих шаблонов. Шаблоны Стратегия и Шаблонный метод можно реализовать с использованием ссылок на код. Строитель обычно строит структуру с использованием некоторого сочетания ссылок на хэши и списки. Интерпретатор может быть реализован с использованием простых средств, вроде split или при помощи всемогущего Parse::RecDescent, который является лучшим генератором синтаксических анализаторов, подобных yacc, внутри скриптов на Perl (хотя он и менее эффективен, чем yacc).

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

В каких случаях хороши объекты?

Поскольку Ларри Уолл (Larry Wall) позаботился обо всех способах написания программ, стоит использовать объекты только тогда, когда они имеют смысл и не использовать, если нет. А когда они имеют смысл? Отчасти это дело вкуса. Этот подраздел описывает то, что нравится мне.

Проще всего объяснить, в каких случаях объекты не подходят. Они не подходят, если попадают в одну из нескольких категорий:
  1. Есть только данные, а методы либо тривиальны, либо несущественны. Контейнеры данных (также называемые узлами) относятся к этому случаю. Например, объект не нужен, если в вызывающий код нужно всего-лишь вернуть три числа и строку.
  2. Есть только методы. Класс Math из Java - этот случай. Он даже не позволяет сделать объект Math. Очевидно, что эти методы должны быть просто встроенными функциями языка.
Наблюдение за плохим использованием объектов позволяет понять, когда их применение наиболее эффективно. Используйте объекты, когда сложность высока, а данные тесно связаны с методами их обработки. Высокая сложность позволяет в полной мере проявить основные преимущества объектов: отдельные пространства имён, наследование и полиморфизм.

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

Абстрактная фабрика

Если нужно написать программу, которая будет использоваться для доступа к нижележащей системе, но не зависеть от платформы, нужен подход, позволяющий не переписывать программу для каждого API. Вот где пригодится фабрика. Программа просит у абстрактной фабрики экземпляр класса, реализующий доступ к используемой платформе. Как мы увидим ниже, платформой может быть база данных. Итак, фабрика возвращает объект, пригодный для доступа к определённой базе данных, но все объекты имеют одинаковый API.

Для демонстрации основной идеи приведу пример, который возвращает один из двух типов. В этом примере четыре файла с исходными текстами. Два первых файла содержат классы для приветствия пользователя.
package Greet::Repeat;
use strict; use warnings;

sub new {
  my $class = shift;
  my $self = {
    greeting => shift,
    repeat => shift,
  };
  return bless $self, $class;
}

sub greet {
  my $self = shift;
  print ($self->{greeting} x $self->{repeat});
}

1;
Конструктор приветствия ожидает строку приветствия и количество повторов. Он сохраняет их в хэш и возвращает "благословлённую" ссылку на этот хэш. Когда будет вызван метод greet, он несколько раз выведет приветствие (отсюда и название класса). (Я не говорю, что этот пример полезен, просто он достаточно маленький.)
package Greet::Stamp;
use strict; use warnings;

sub new {
  my $class = shift;
  my $greeting = shift;
  return bless \$greeting, $class;
}

sub greet {
  my $greeting = shift;
  my $stamp = localtime();
  print "$stamp $$greeting";
}

1;
Этот класс приветствия ожидает только строку приветствия, поэтому "благословляет" ссылку на неё. Когда вызывается метод greet, но выводит текущее время с последующей строкой приветствия.

Теперь - фабрика:
package GreetFactory;
use strict; use warnings;

sub instantiate {
  my $class = shift;
  my $requested_type = shift;

  my $location = "Greet/$requested_type.pm";
  my $class = "Greet::$requested_type";

  require $location;
  return $class->new(@_);
}

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

Теперь можно воспользоваться этой фабрикой следующим образом:
#!/usr/bin/perl
use strict; use warnings;

use GreetFactory;

my $greeter_n = GreetFactory->instantiate("Repeat", "Hello\n", 3);
$greeter_n->greet();

my $greeter_stamp = GreetFactory->instantiate("Stamp", "Good-bye\n");
$greeter_stamp->greet();
Для создания каждого из объектов приветствия вызывается метод instantiate класса GreetFactory, которому передаётся имя требуемого класса и любые аргументы, которые принимает конструктор этого класса.

Этот пример демонстрирует основную идею. У него простая цель. Но он показывает, как фабрика может абстрагироваться от нижележащих классов. Каждый новый класс приветствия, добавленный в систему, должен иметь имя вида Greet::Имя и должен располагаться в каталоге Greet в пути поиска @INC под именем Имя.pm. Если это так, можно использовать его без изменения фабрики. Это был лишь учебный пример, а теперь посмотрим на пример из практики.

Модуль DBI (DataBase Interface) из Perl демонстрирует великолепный пример фабрики. Каждый вызов DBI->connect принимает тип базы данных и информацию, необходимую для подключения к этой базе данных. Это классическая фабрика. Она загружает любой модуль DBD (DataBase Driver), который установлен в системе, в соответствии с запросом. Дополнительные модули DBD могут быть добавлены в любой момент. Если модуль установлен, любой клиент сможет использовать его при помощи того же API модуля DBI. Вот пример использования DBI:
use DBI;

my $dbh = DBI->connect("dbi:mysql:mydb:localhost", "user", "password");
...
my $sth = $dbh->prepare('select * from table');
...
Однажды полученный дескриптор базы данных (который обычно называют $dbh) может использоваться без оглядки на нижележащую реализацию. Если позже потребуется перевести программу на Oracle, потребуется лишь изменить вызов connect. Если появится новая база данных, то какой-то умный человек вместе с Тимом Бансом (Tim Bunce) реализует класс для работы с ней. Как только они закончат свою работу, можно будет установить модуль и переключиться на его использование. Может быть даже новый модуль напишете вы, но я - вряд ли.

Композиция

Теперь рассмотрим, как использовать полностью объектно-ориентированный шаблон Композиция. Если вы заинтересованы в более простой, не объектно-ориентированной реализации этого шаблона, обратитесь к разделу Строитель в предыдущей статье.

Многие приложения нужно выстраивать в иерархии взаимосвязанных элементов. Многие люди представляют эту иерархию в виде структуры каталогов. Наверху - корневой каталог. В простейшем случае есть два типа элементов: файлы и каталоги. Каждый подкаталог похож на корневой. Отметим, что это определение структуры - рекурсивное, что характерно для композиций.

В наши дни один из наиболее популярных примеров композиции - это файл XML. У этих файлов есть корневой элемент, содержащий подчинённые элементы различных типов, включая тэги и комментарии. Тэги обладают атрибутами и иногда могут содержать другие подчинённые элементы. Образуется обычное дерево композиции. Выделяют два важных этапа: построение композиции и её использование. Рассмотрим оба этапа на простом примере.

Шаблон в чистом виде должен содержать метод, взаимодействующий и с обычными и с составными элементами (то есть с элементами, содержащими в себе другие элементы). Вызов такого метода в корне дерева композиции или корне поддерева приводит к тому, что корень работает над собственными данными И транслирует запрос потомкам. Потомки делают то же самое, собирая собственные данные и данные своих потомков до тех пор, пока не будет обработано всё нижележащее дерево. Возвращаемое значение - это коллекция всех данных.

За практическим примером обратимся к использованию модели DOM для обработки XML. (Модуль XML::DOM можно взять на CPAN.) Чтобы найти все параграфы в документе, нужно поступить примерно так:
use XML::DOM;

my $parser = XML::DOM::Parser->new();
my $doc = $parser->parsefile("file.xml");

foreach my $paragraph ($doc->getElementsByTagName("paragraph")) {
  print "<p>";
  foreach my $child ($paragraph->getChildNodes) {
    print $child->getNodeValue if ($child->getNodeType eq TEXT_NODE);
  }
}

$doc->dispose();
Вызов getElementsByTagName начинается с корневого элемента (потому что он вызван у объекта $doc). Корень возвращает все дочерние элементы, которые являются параграфами, но он также передаёт запрос ко всем своим элементам-тегам, запрашивая их вернуть их параграфы. Они делают то же самое.

Примечание не по теме: Отметим, что вышеприведённый пример завершается вызовом dispose. Композиция XML::DOM использует ссылки от родителей к потомкам и от потомков к родителям. Обычно это приводит к образованию циклических ссылок. Сборщик мусора в Perl 5 не умеет убирать такие структуры данных. Необходимо вызывать dispose, чтобы порвать "лишние" ссылки и сделать возможным восстановление свободной памяти. Если вы строите структуры данных с циклическими ссылками, вам нужно разрывать такие ссылки самостоятельно, в противном случае произойдёт утечка памяти.

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

Для примера рассмотрим не двоичное дерево (о котором и шла речь выше). Предположим, что нужно узнать, сколько узлов в дереве. Можно спросить об этом корень, вызвав метод count_nodes. Он должен посчитать и добавить себя к сумме, которую вернут вызовы метода count_nodes для каждого из потомков. Узлы, не являющиеся составными (то есть не имеющие потомков), возвращают единицу. Составные узлы возвращают единицу плюс сумму, полученную от своих потомков. Далее будет приведён пример.

Пример состоит из четырёх частей: (1) базовый класс для узлов дерева: Node.pm, (2) класс для узлов, у которых могут быть потомки: Composite.pm, (3) класс для узлов, у которых не может быть потомков: Regular.pm, и (4) управляющая программа для демонстрации работы системы: comp. Я приведу их исходные тексты сразу, в том порядке, в котором они были перечислены.
package Node;
use strict; use warnings;

sub count_nodes {
  my $self = shift;
  my $class_name = ref $self;
  die "$class_name не реализует count_nodes\n";
}

1;
Единственный метод здесь - это count_nodes. Он является заглушкой для настоящей реализации метода (такие методы также называются абстрактными). Попытка воспользоваться подклассом Node, не имеющем собственной реализации count_nodes приведёт к фатальной ошибке времени выполнения. Каждый подкласс должен иметь соответствующую проверку, чтобы убедиться, что эта ошибка никогда не произойдёт при его использовании.
package Regular;
use Node;

@ISA = qw(Node);
use strict; use warnings;

sub new {
  my $class = shift;
  my $name = shift;

  return bless \$name, $class;
}

sub count_nodes {
  return 1;
}

1;
Обычные узлы являются ссылками, "благословлёнными" их именами. Они всегда насчитывают только один узел. (Отвлечённое замечание: иногда бывает удобно включить прагму strict после преамбулы пакета, чтобы позволить себе воспользоваться @ISA, не объявляя его явно.)
package Composite;
use Node;

@ISA = qw(Node);
use strict; use warnings;

sub new {
  my $class = shift;
  my $name = shift;

  my $self = { name => $name, children => [] };
  return bless $self, $class;
}

sub add_child {
  my $self = shift;
  my $new_child = shift;

  push @{$self->{children}}, $new_child;
  return $new_child;
}

sub count_nodes {
  my $self = shift;

  my $count = 1;
  foreach my $child (@{$self->{children}}) {
    $count += $child->count_nodes();
  }
  return $count;
}

1;
Этот класс аналогичен классу Regular, но в нём должно быть место для хранения ссылок на потомков. Поскольку он также содержит собственное имя, здесь "благословляется" хэш. Новые потомки просто помещаются в список. Подсчёт учитывает один родительский узел и полное количество узлов каждого из потомков. Поскольку листья дерева тоже реализуют count_nodes, можно обрабатывать все объекты, реализующие класс Node, одновременно. Это преимущество полиморфизма объектов, которое и является основополагающей концепцией шаблона Композиция.
#!/usr/bin/perl
use strict; use warnings;

use Composite;
use Regular;

my $root = Composite->new("Root");

my $eldest   = $root->add_child(Composite->new("Jim"));
my $middle   = $root->add_child(Composite->new("Jane"));
               $root->add_child(Regular->new("Bob"));
my $youngest = $root->add_child(Composite->new("Joe"));

            $eldest->add_child(Regular->new("JII"));
my $kayla = $eldest->add_child(Composite->new("Kayla"));
            $kayla->add_child(Regular->new("Max"));

my $count = $root->count_nodes();

print "Количество: $count\n";
В этом надуманном примере происходит ручное построение простого дерева и запрос количества узлов в нём. Правильный ответ - 8.

Заместитель

В примере шаблона заместителя из книги банды четырёх иллюстрируется способ отложенной загрузки ресурсоёмких компонентов до того момента, пока они действительно не понадобятся пользователю. В ходе примера они демонстрируют настоящего заместителя. Заместитель перенаправляет все запросы к другому объекту. Думайте о нём как об организаторе убийства. Вы передаёте ему заказ, как будто он будет выполнять его сам. Он передаёт заказ киллеру, которого вы никогда не видели, но именно он и выполнит работу. (Примечание для Джона Эшкрофта (John Ashcroft): я только вообразил этот процесс, но у меня НЕТ личного опыта. Честно.)

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

Вот класс, который на самом деле сохраняет и выводит файлы:
package File;
use strict; use warnings;

sub new {
  my $class = shift;
  my $file = shift;

  open FILE, "$file" or die "Не могу прочитать файл $file: $!\n";
  my @data = <FILE>;
  close FILE;

  return bless \@data, $class;
}

sub print_file {
  my $data = shift;

  print @$data;
}

sub DESTROY { }

1;
Когда вызывается конструктор File, он читает файл в массив для последующего использования, возвращая вызывающему коду "благословлённую" ссылку на данные. Когда запрошена печать, он отправляет данные в текущий дескриптор вывода (обычно это стандартный вывод).

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

В классе File, показанном выше, нет ничего особенного. Теперь рассмотрим класс-заместитель.
package FileProxy;
use strict; use warnings;
use File;

sub new {
  my $class = shift;

  my $self = {
    params => \@_,
    wrapped_object => undef,
  };

  return bless $self, $class;
}

sub AUTOLOAD {
  my $self = shift;
  my $command = our $AUTOLOAD;

  $command =~ s/.*://;
  unless (defined $self->{wrapped_object}) {
    $self->{wrapped_object} = File->new(@{$self->{params}});
  }

  $self->{wrapped_object}->$command(@_);
}

1;
Конструктор заместителя принимает аргументы, необходимые для создания настоящего объекта File (а конкретнее - имя файла) и сохраняет его в качестве своего атрибута params. Другой атрибут в конечном счёте будет содержать обёрнутый объект File. Атрибуты сохраняются в хэше, ссылка на хэш "благословляется" и возвращается вызывающему коду.

Каждый раз, когда Perl не может найти вызываемый метод, он вызывает AUTOLOAD (как в примере выше). Метод AUTOLOAD в FileProxy обрабатывает все запросы, за исключением методов new и DESTROY, которые определены явным образом. AUTOLOAD пишется в верхнем регистре, чтобы напомнить нам о том, что Perl вызывает этот метод самостоятельно. Когда выполняется вызов, Perl записывает в глобальную переменную пакета $AUTOLOAD имя метода, который был вызван. Регулярное выражение отрезает имя пакета от $AUTOLOAD, оставляя только имя метода.

Если объект ещё не определён, AUTOLOAD вызовет File->new, передав ему аргументы, сохранённые при создании самого объекта-заместителя. Затем, когда объект уже определён, AUTOLOAD вызовет запрошенный метод обёрнутого объекта. Красота этого механизма заключается в том, что класс FileProxy знает только о существовании конструктора new. Его не нужно менять, если внесены изменения в File.pm. Любые ошибки, например вызов не существующего метода, по прежнему окажутся фатальными.

Для использования этой схемы замещения можно написать, например, следующий код:
#!/usr/bin/perl
use strict; use warnings;
use FileProxy;

my $file1 = FileProxy->new("art1");
my $file2 = FileProxy->new("art2");

$file1->print_file();
$file1->print_file();
$file2->print_file();
Если внести небольшие изменения, то можно будет использовать заместителя для работы с любым классом. Вот новая, обобщённая версия:
package DelayLoad;
use strict; use warnings;

our %proxied_classes;

sub import {
  shift; # пропускаем имя класса

  %proxied_classes = @_;

  foreach my $class (keys %proxied_classes) {
    require "$class.pm";
  }
}

sub new {
  my $class = shift;

  my $self = {
    type => shift,
    constructor => shift,
    params => \@_,
    wrapped_object => undef,
  };
  return bless $self, $class;
}

sub AUTOLOAD {
  my $self = shift;
  my $command = our $AUTOLOAD;
  
  $command =~ s/.*://;
  if ($proxied_classes{$command}) {
    return $self->new($command, $proxied_classes{$command}, @_);
  }
  else {
    unless (defined $self->{wrapped_object}) {
      my $proxied_class = $self->{type};
      my $constructor = $self->{constructor};
      $self->{wrapped_object} = $proxied_class->$constructor(@{$self->{params}});
    }
    $self->{wrapped_object}->$command(@_);
  }
}

1;
Первое изменение - косметическое: имя теперь отражает природу заместителя. Другие изменения включают новый метод - import. Хотя имя метода записано в нижнем регистре, тем не менее Perl вызывает его всякий раз, когда вызывающий код говорит "use DelayLoad" (смотрите ниже). Он делает две вещи. Во-первых, он сохраняет имя каждого замещённого класса в глобальный хэш пакета %proxied_classes. Во-вторых, он подключает каждый модуль при помощи require. require подобен use, но выполняется во время работы программы, а не при её компиляции. (use также импортирует таблицу имён, но объектно-ориентированный модуль обычно ничего не экспортирует.)

Теперь конструктор хранит чуточку больше информации. Кроме подготовки места для хранения обёрнутого объекта и хранения параметров, он также записывает имя класса и имя его конструктора. Всё это используется в AUTOLOAD.

Есть и другие изменения в методе AUTOLOAD. В нём есть два изменения. Простейшее - это поиск класса и имени конструктора в объекте DelayLoad вместо прямого вызова File->new.

Другое изменение используется в процессе создания объекта. Моё объяснение будет более доходчивым, если сначала посмотреть на вызывающий код.

В новой версии нужно внести несколько изменений в вызывающий код. Одно из изменений заключается в том, что строка use превращается в:
use DelayLoad "File" => "new";
В этой строке мы сообщаем DelayLoad, что мы хотим его использовать для отложенной загрузки объектов File, а конструктор объектов File называется new.

Второе изменение касается того, как создаётся обёрнутый объект:
my $file1 = DelayLoad->File("art1");
my $file2 = DelayLoad->File("art2");
Здесь объясняется необъяснённый фрагмент AUTOLOAD. Когда пользователь вызывает метод File, AUTOLOAD замечает, что этот "метод" на самом деле является именем класса, для которого должна быть выполнена отложенная загрузка. Если условие if в AUTOLOAD истинно (то есть метод на самом деле является ключом в хэше %proxied_classes), вызывающий код получает новый объект DelayLoad, подготовленный к дальнейшему использованию. Если условие if не срабатывает, DelayLoad работает как FileLoad: он создаёт объект, если это необходимо, и вызывает запрошенный метод.

Главное в этом примере заключается в том, что Perl позволяет нам реализовать заместителя практически без необходимости что-либо знать о нижележащем классе. В этом случае import получает необходимую информацию от вызывающего кода, а AUTOLOAD заботится об остальном. Перекладывать часть работы на вызывающий код - не самая лучшая мысль. Здесь в этом есть смысл, поскольку если нужно выполнить отложенную загрузку объектов, то хотя-бы нужно знать API этих объектов. В данном случае под API понимается имя конструктора, которое указывается в выражении use для того, чтобы Perl мог передать его в метод DelayLoad::import.

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

Итог

В этой статье окончательно продемонстрированы объектно-ориентированные шаблоны. Мы увидели, как реализовать Фабрику так, чтобы вызывающий код мог выбирать необходимый драйвер, как строить составные структуры данных и подпрограммы для их обхода (без явного использования указателей first_child и next, что было бы необходимо в случае языков без качественной встроенной поддержки списков), и как стать заместителем между вызывающим кодом и классом при помощи методов import и AUTOLOAD.

Примечания автора

Это последняя статья из серии статей, но в скором будущем вы сможете купить в ближайшем книжном магазине книгу Design Patterns in Perl (Шаблоны проектирования в Perl), выпущенную издательством Apress.

воскресенье, 17 ноября 2013 г.

Шаблоны проектирования в Perl, часть 2

Перевод статьи: Perl Design Patterns, Part 2
Автор: Фил Кроу (Phil Crow)

Это вторая статья из серии статей, составляющих единый ответ программистов Perl на книгу "Шаблоны проектирования" (также известную как "книга банды четырёх", поскольку её написали четверо авторов).

Как было показано в первой статье, Perl предоставляет лучшие шаблоны в составе своего ядра и многих модулей, поставляемых в его составе или доступных на CPAN. Были рассмотрены Итератор (foreach), Декоратор (каналы Unix и фильтры списков), Приспособленец (Memoize.pm) и Одиночка ("благословение" объекта в блоке BEGIN).

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

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

Контейнеры данных

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

Вот конкретный пример. Предположим, что мне нужен список телефонов. Я могу воспользоваться вот таким контейнером:
my $phone_list = {
  'Phil' => [
    { type => 'home', number => '555-0001' },
    { type => 'pager', number => '555-1000' },
  ],
  'Frank' => [
    { type => 'cell', number => '555-9012' },
    { type => 'pager', number => '555-5678' },
    { type => 'home', number => '555-1234' },
  ],
};
Этот контейнер располагается в хэше. Его ключи - это имена; его значения - телефонные номера. Номера помещаются в список в том порядке, в котором желательно их использовать. (Звонить Фрэнку сначала на сотовый телефон, потом на пэйджер. Если дозвониться на них не удалось, тогда можно попробовать домашний телефон.)

Я могу пользоваться этой структурой следующим образом:
#!/usr/bin/perl

use strict; use warnings;

my $phone_list = {
  'Phil' => [
    { type => 'home', number => '555-0001' },
    { type => 'pager', number => '555-1000' },
  ],
  'Frank' => [
    { type => 'cell', number => '555-9012' },
    { type => 'pager', number => '555-5678' },
    { type => 'home', number => '555-1234' },
  ],
};

my $person = shift or die "Способ использования: $0 человек\n";

foreach my $number (@{$phone_list->{$person}}) {
  print "$number->{type} $number->{number}\n";
}
В этом примере пользователь указывает имя человека, которому нужно дозвониться, в командной строке, в качестве аргумента, который программа сохраняет в переменную $person. Затем программа перебирает все телефонные номера этого человека, выводя тип номера и сам номер.

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

Если нужно воспользоваться структурой, состоящей из данных в её узлах, зачастую можно обойтись без объекта-узла при помощи контейнера данных. Сторонники объектно-ориентированного программирования скорее всего захотят сделать каждого человека отдельным объектом. В таком объекте они захотят хранить объект с телефонным номером каждого типа в некоем громоздком контейнере-списке. Совет: не становитесь педантами. Даже в Java можно создать структуру, подобную приведённой выше (хоть это и не просто). Часто целесообразно поступать именно так. Объекты лучше использовать в более сложных случаях.

Что такое ссылка на код?

Ссылка на код подобна любой другой ссылке в Perl, но она указывает на подпрограмму, которую можно вызвать. Например, я могу написать:
my $doubler = sub { return 2 * $_[0]; };
А затем я могу вызвать эту подпрограмму:
my $doubled = &$doubler(5); # Теперь $doubled равно 10
Это надуманный пример, но он демонстрирует основы синтаксиса ссылок на код. Если присвоить подпрограмму переменной, получится ссылка на код, благодаря Perl. Чтобы вызвать сохранённую в ссылке подпрограмму, поместите перед переменной знак &. То же самое обычно делается обычно при переборе элементов хэша:
foreach my $key (keys %$hash_reference) { ... }
Знак & - это сигил (или забавный символ) для подпрограмм, как @ и % - сигилы для массивов и хэшей.

Многие шаблоны банды четырёх и за её пределами можно легко реализовать в Perl при помощи ссылок на код. Языки, не имеющие ссылок на код, остались без важного типа данных.

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

Стратегия

Когда нужно выбрать какое-то одно действие из некоего множества, пригодится шаблон "Стратегия". Например, может потребоваться выполнить сортировку с использованием определённой функции сравнения. При каждой сортировке имеется возможность задать стратегию сортировки.

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

Вот пример встроенной сортировки:
sort { lc($a) cmp lc($b) } @items
Это сортировка без учёта регистра. Отметим, что функция сравнения определяется вместе с вызовом sort. Хотя можно сделать для этого отдельные функции, более распространён приём, при котором функция сравнения передаётся в качестве аргумента для позиционного параметра, требующего ссылку на функцию.

Предположим, что нужно получить список всех файлов с определённым свойством в текущем каталоге или его подкаталогах. Есть две части задачи: (1) просканировать дерево каталогов, и (2) проверить каждый файл на соответствие условию. Лучше, если можно разделить эти задачи, чтобы потом можно было повторно использовать их поотдельности (например, просто просканировать дерево каталогов, без проверки каких-либо условий). Проверку условия можно сделать стратегией, выполняемой сканером каталога.
#!/usr/bin/perl

use strict; use warnings;

my @files = find_files(\&is_hidden, ".");
local $" = "\n";
print "@files\n";

sub is_hidden {
  my $file = shift;
  $file =~ s!.*/!!;
  return 0 if ($file =~ /^\./);
  return 1;
}

sub find_files {
  my $callback = shift;
  my $path = shift;
  my @retval;

  push @retval, $path if &$callback($path);
  return @retval unless (-d $path);

  # Определился подкаталог
  opendir DIR, $path or return;
  my @files = readdir DIR;
  closedir DIR;

  # Обходим каждый подкаталог
  foreach my $file (@files) {
    next if ($file =~ /^\.\.?$/); # Пропускаем каталоги . и ..
    push @retval, find_files("$path/$file", $callback);
  }

  return @retval;
}
Для понимания примера начнём с первого вызова find_files. В него передаётся два аргумента. Первый - это ссылка на код. Обратите внимание на синтаксис. Как я уже упомянул во введении, чтобы Perl знал, что подразумевается подпрограмма, перед is_hidden нужно поместить сигил &. Чтобы сделать ссылку на подпрограмму (вместо её немедленного вызова), нужно поместить перед ней обратную косую черту, точно так же, как и для получения ссылок других типов.

При использовании коллбэка в find_files, $callback содержит ссылку на код. Для её разыменования нужно поместить перед ней сигил &.

Подпрограмма find_files получает путь, с которого нужно начинать поиск и ссылку на код с именем $callback. При каждом вызове подпрограмма сохраняет путь в возвращённом списке, если коллбэк возвращает для этого пути истину. Это позволяет повторно использовать find_files во многих других программах, изменяя только коллбэк, чтобы изменить результат. Это шаблон "Стратегия", но без надоедливого наследования от абстрактного базового класса find_files и замены метода с условием отбора.

В find_files используется рекурсия для спуска по дереву каталогов. Сначала вызвается коллбэк, чтобы удостовериться, что этот путь должен попасть в результат. Затем начинается настоящая подпрограмма. Чем занимается коллбэк - для этой подпрограммы не важно. find_files примет любое значение - истину или ложь.

Рекурсия прекращается, если файл не является каталогом. В этом случае немедленно возвращается список. (Он может быть пустым или содержать текущий путь, в зависимости от значения, возвращённого коллбэком.) В противном случае, все файлы и каталоги в текущем пути читаются в @files. Каждый из элементов сканируется рекурсивно при помощи find_files (если это не каталог . или .., которые приведут к бесконечной рекурсии). Что бы ни вернул рекурсивный вызов find_files, его результат помещается в конец итогового списка. Когда все подкаталоги проверены, вызывающему коду возвращается @result.

Модуль File::Find на CPAN решает ту же задачу более основательно, чем в примере выше. Он исользует точно такую же разновидность коллбэков.

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

Шаблонный метод

В некоторых расчётах известны этапы, но заранее не известно, что делает каждый из этапов. Например, расчёт арендных выплат может состоять из трёх этапов:
  1. Рассчитать сумму по ставкам.
  2. Посчитать налоги.
  3. Сложить всё вместе.
Разные арендодатели могут использовать разные схемы расчёта суммы по ставкам, а в разных ведомствах обычно разные схемы налогообложения. Шаблонный метод может реализовать план, детали которого зависят от индивидуальных схем расчёта, используемых вызывающим кодом.
package Calc;
use strict; use warnings;

sub calculate {
  my $class = shift; # отбрасывается
  my $data = shift;
  my $rate_calc = shift; # ссылка на код
  my $tax_calc = shift; # тоже ссылка на код

  my $rate = &$rate_calc($data);
  my $taxes = &$tax_calc($data, $rate);
  my $answer = $rate + $taxes;
}
В этом примере вызывающий код предоставляет ссылку на данные (например, на хэш или на объект) совместно с двумя ссылками на код, которые используются в качестве коллбэков. Каждый из коллбэков должен ожидать ссылку на данные в первом параметре. Ссылка на код tax_calc code также принимает сумму по ставкам. Это позволяет использовать процент от суммы вместе с информацией по ссылке на данные.

Вызывающий код может выглядеть следующим образом:
#!/usr/bin/perl

use strict; use warnings;
use Calc;

my $rental = {
  days_used => 5,
  day_rate => 19.95,
  tax_rate => .13,
};

my $amount_owed = Calc->calculate($rental, \&rate_calc, \&taxes);
print "С вас причитается $amount_owed\n";

sub rate_calc {
  my $data = shift;
  return $data->{days_used} * $data->{day_rate};
}

sub taxes {
  my $data = shift;
  my $subtotal = shift;

  return $data->{tax_rate} * $subtotal;
}
Этот надуманный пример показывает последовательность вызовов. Данные в этом примере - это просто хэш. Чтобы уменьшить таблицу экспорта из модуля Calc, я сделал функцию calculate методом класса, поэтому её нужно вызывать через класс. При вызове передаются аргументы - ссылка на хэш с данными и ссылки на две подпрограммы для расчётов.

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

Другой подход к шаблонизации заключается в том, чтобы поместить вызовы методов в пакет с шаблоном. Этот подход основывается на использовании примесей (mixin), как в Ruby. Вот более объектно-ориентированный пример.
package Calc;

sub calculate {
  my $self = shift;
  my $rate = $self->calculate_rate();
  my $tax = $self->calculate_tax($rate);
  return $rate + $tax;
}

1;
Весь модуль фактически состоит лишь из одного шаблонного метода. Для его использования нужно определить методы calculate_rate и calculate_tax, в противном случае скрипт завершится с ошибкой. Вот возможная реализация этого класса:
package CalcDaily;
package Calc;

use strict; use warnings;

sub new {
  my $class = shift;
  my $self = {
    days_used => shift,
    day_rate => shift,
    tax_rate => shift,
  };
  return bless $self, $class;
}

sub calculate_rate {
  my $data = shift;
  return $data->{days_used} * $data->{day_rate};
}

sub calculate_tax {
  my $data = shift;
  my $subtotal = shift;
  return $data->{tax_rate} * $subtotal;
}

1;
Отметим, что я добавил конструктор и два метода к пакету Calc в другом файле. Это совершенно законно и иногда полезно, при этом шаблон полностью изолирован. Он совершенно не знает о том, какой вид данных хранится в объектах его типа. Это означает, что одновременно может использоваться только один подтип Calc. Если это не удобно, можно прибегнуть к обычному решению: поместить методы, которые вызываются из Calc, в объекты отдельных иерархий.

Два выражения package в начале файла добавлены в определённых целях. Первое говорит о том, что это пакет CalcDaily, который по праву принадлежит файлу CalcDaily.pm, а не исходный Calc, принадлежащий Calc.pm.

Наконец, приведём пример слегка изменённого вызывающего кода:
#!/usr/bin/perl

use strict; use warnings;
use Calc;
use CalcDaily;

my $rental = Calc->new(5, 19.95, .13);
my $amount_owed = $rental->calculate();
print "С вас причитается $amount_owed\n";
Приём похож на тот, который используется в архитектуре отладчика Perl. Чтобы сделать собственный отладчик, нужно придумать для него имя. Допустим, это PhilDebug.pm. Тогда я создаю файл с таким именем в каталоге Devel, находящемся среди каталогов из списка @INC. Первой строкой файла должна (но не обязана) быть:
package Devel::PhilDebug;
Так индексатор CPAN сможет найти каталог с этим модулем.

Базовый пакет для отладчиков - это пакет DB. Perl ожидает, что в этом пакете есть функция DB. Поэтому вместе это может выглядеть следующим образом:
package Devel::PhilDebug;
package DB;

sub DB {
  my @info = caller(0);
  print "@info\n";
}

1;
Любой скрипт сможет использовать этот отладчик, если его вызвать следующим образом:
perl -d:PhilDebug script
Каждый раз, когда отладчик уведомляется о том, что началось новое выражение, он сначала вызовает DB::DB. Это очень мощный пример plug-and-play, то есть изменения системы путём добавления компонента.

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

Не стоит запрещать опасное. Стоит просто избегать этого, пока не появится достаточно разумная причина.

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

Строитель

Многие внешние по отношению к программе структуры внутри программы должны быть представлены составными структурами (как деревья или контейнеры данных во введении). Есть два основных подхода к представлению данных в этих структурах. В объектно-ориентированном подходе для составления таких структур используется шаблон Композиция (который будет обсуждаться в следующей статье).

Сейчас же мы рассмотрим, как создать составную структуру на основе хэша хэшей. Возможно вы предпочтёте создать объектно-ориентированную версию такой структуры. Что выбрать - зависит от сложности данных и методов их обработки. Если данные и методы просты, скорее всего вы захотите использовать просто хэши. Они быстрее, их поддержка встроена в Perl и их проще понять Perl-программистам, которым может потребоваться сопровождать ваш код. Если сложность велика, лучше воспользоваться полновесными объектами. Их структура проще воспринимается объектно-ориентированными программистами и позволяет достичь большей самодокументированности, чем при использовании простых хэшей.

Итак, хэши - это превосходные структуры для хранения составных данных различной сложности: от простых до умеренно сложных. Чтобы увидеть, как построить структуру на основе хэша, обратимся к примеру: визуализация плана. Для простоты я представляю план простыми отступами (а не римскими числами или какими-то другими способами нумерации). Вот пример плана:
Бакалейная лавка
  Молоко
  Сок
  Мясник
    Тонкие ломтики ветчины
    Жареный цыплёнок
  Сыр
Очистители
Товары для дома
  Дверь
  Замок
  Шайба
Этот план описывает предполагаемый маршрут покупок. Нужно представить его внутри программы так, чтобы с ним можно было работать. (Одна из моих любимых игр - превращать план в картинку, смотрите ниже.)

Вместо полномасштабного объекта я воспользуюсь небольшим контейнером данных на основе хэша, для каждого узла дерева. Каждый узел содержит одну из следующих сущностей:
  1. Название
  2. Уровень вложенности
  3. Дочерние элементы (список других узлов)
Для сохранения иерархии, кто чьим дочерним элементом является, воспользуемся стеком узлов. Узел на вершине стека обычно является родительским для следующей поступающей строки. Для иллюстрации этого метода я снабдил скрипт комментариями. В конце этого раздела скрипт будет приведён целиком.
#!/usr/bin/perl

use strict; use warnings;
Эти строки всегда полезны.
my $root = {
  name => "ROOT",
  level => -1,
  children => [],
};
Это корневой узел. Он является ссылкой на хэш, содержащий три ключа, описанных выше. Корневой узел - особый. Поскольку его нет в файле, ему назначено искусственное имя и уровень, меньший чем у какого-либо другого узла. (Кстати, предполагается, что уровень вложенности поступающих данных может быть нулевым или положительным.) Начальный список дочерних элементов пуст.
my @stack;
push @stack, $root;
Стек будет содержать предков каждого нового узла. Для начала нужен корневой узел, который никогда не вставляется в стек, потому что является предком для всех узлов.
while (<>) {
  /^(\s*)(.*)/;
  my $indentation = length $1 if defined ($1);
  my $name = $2;
Для чтения строк из файла используется цикл while. Каждая строка состоит из двух частей: отступа (ведущих пробелов) и имени (остаток строки). Регулярное выражение захватывает ведущие пробелы в строку $1, а всё остальное (за исключением символа новой строки) - в строку $2. Чем больше длина отступа, тем больше предков имеет узел. Строки, начинающиеся у края, имеют нулевой отступ (вот почему узел ROOT имеет уровень -1).
while ($indentation <= $stack[-1]{level}) {
    pop @stack;
  }
Этот цикл обрабатывает предков. Он помещает в стек новый элемент, если узел на верхушке стека является родителем нового узла. Например, когда мы заходим в Товары для дома, Очистители и ROOT уже находятся в стеке. Уровень Товаров для дома - нулевой (он находится на краю), как у Очистителей. Однако, Очистители уже находятся в стеке (поскольку 0 <= 0). Тогда остаётся только ROOT, поэтому вставка прекращается (0 не <= -1).
my $node = {
    name => $name,
    level => $indentation,
    children => [],
  };
Так создаётся новый узел для текущей строки. Задаются его имя и уровень. Пока что у него нет детей, но создаётся место для них - пустой список.
push @{$stack[-1]{children}}, $node;
Эта строка добавляет новый узел к списку детей его родителя. Отметим, что родитель находится на верхушке стека. Верхушка стека - это $stack[-1] или последний элемент массива.
push @stack, $node;
}
Здесь новый узел кладётся в стек, если у него есть дети. Закрывающая скобка завершает цикл чтения строк. Для простоты результат отображается при помощи Data::Dumper:
use Data::Dumper; print Dumper($root);
Выполнение этой строки выведет дерево (растущее вправо) на стандартный вывод.

Весь код целиком:
#!/usr/bin/perl

use strict; use warnings;

my $root = {
  name => "ROOT",
  level => -1,
  children => [],
};

my @stack;
push @stack, $root;

while (<>) {
  /^(\s*)(.*)/;
  my $indentation = length $1;
  my $name = $2;
  while ($indentation <= $stack[-1]{level}) {
    pop @stack;
  }

  my $node = {
    name => $name,
    level => $indentation,
    children => [],
  };

  push @{$stack[-1]{children}}, $node;
  push @stack, $node;
}

use Data::Dumper; print Dumper($root);
Я обещал объяснить, как подобные структуры можно превратить в картинки. Модуль UML::Sequence из CPAN строит структуру, похожую на показанную здесь. Затем он использует её для того, чтобы сгенерировать последовательность диаграмм UML по этапам в формат SVG (Scalable Vector Graphics - масштабируемая векторная графика). Этот формат можно сконвертировать при помощи стандартных инструментов, например Batik, в PNG или JPEG. На практике контуры, которые я преобразую в картинки, изображают последовательность вызовов программ. Perl даже может сгенерировать контур, запустив программу. За более подробной информацией обратитесь к UML::Sequence.

Если понадобится прочитать какие-то данные с необычной структурой, строитель может помочь в формировании соответствующей внутренней структуры данных. Один из таких строителей - XML::DOM. Другой, XML::Twig, обладает несколько другим подходом. Не случайно, что средства для чтения XML являются строителями, потому что файлы XML - это не-двоичные деревья.

Интерпретатор

Если вы ещё не смотрели книгу банды четырёх, начните с шаблона "Интерпретатор". Посмейтесь от души. Человек, который сообщил мне об этом шаблоне в Java не понимал, почему этот шаблон на практике не работает. Он слышал, что этот шаблон слишком медленный, но не был уверен. Зато уверен я.

К нашему счастью, в Perl есть альтернативы. Это широкий спектр альтернатив от быстрых и грязных до полномасштабных решений. Мантру образуют следующие примеры:
  • Расщепление строк при помощи split
  • Вычисление Perl-кода при помощи eval
  • Модуль Config::Auto
  • Модуль Parse::RecDescent
Поскольку у нас уже есть язык, который нам нравится (если кто не в курсе - это Perl), применение шаблона "Интерпретатор" ограничивается малыми языками. Обычно это бывают файлы конфигурации, поэтому остановлюсь на них. (Смотрите раздел "Строитель", если данные из файла можно представить в виде дерева.)

Расщепление строк

Простейшее решение - воспользоваться split. Предположим, что есть файл конфигурации, содержащий настройки вида переменная=значение. Комментарии и пустые строки игнорируются, а все остальные строки должны состоять из пары переменная-значение. Это просто:
sub parse_config {
  my $file = shift;
  my %answer;

  open CONFIG, "$file" or die "Не могу прочитать файл конфигурации $file: $!\n";

  while (<CONFIG>) {
    next if (/^#|^\s*$/); # Пропускаем пустые строки и комментарии
    my ($variable, $value) = split /=/;
    $answer{$variable} = $value;
  }
  close CONFIG;

  return %answer;
}
Эта подпрограмма принимает имя файла конфигурации. Она открывает его и читает. Внутри цикла чтения строк регулярное выражение отбрасывает строки, начинающиеся с "#" и состоящие только из пробельных символов. Все другие строки делятся по знаку "=". Переменные становятся ключами хэша %answer. После чтения всех строк подпрограмма возвращает хэш.

Обработка строк могла бы быть и более сложной, но сначала ознакомьтесь со следующими подходами (обратите особое внимание на Config::Auto).

Вычисление Perl-кода

Мой любимый способ превратить файл конфигурации в программу на Perl - написать его на Perl. Например, файл конфигурации может быть таким:
our $db_name = "projectdb";
our $db_pass = "my_special_password_no_one_will_think_of";

our %personal = (
  name => "Фил Кроу",
  address => "philcrow2000@yahoo.com",
);
Всё, что нужно для его использования в программе на Perl, это вычислить его при помощи eval:
...
open CONFIG, "config.txt" or die "Не могу...\n";
my $config = join "", ;
close CONFIG;

eval $config;
die "Не удалось вычислить ваш файл конфигурации: $@\n" if $@;
...
Перед чтением файла он открывается, затем используется join и оператор чтения в списковом контексте. Это позволяет поместить весь файл в скаляр. Как только он оказывается там (и для порядка - файл оказывается закрытым), прочитанная строка просто вычисляется при помощи eval. Затем нужно проверить $@, чтобы убедиться, что файл был правильной программой на Perl. Затем можно просто использовать значения переменных точно так же, как будто они изначально были в программе.

Модуль Config::Auto - для тех, кто не хочет суетиться

Если вы слишком ленивы для того, чтобы писать собственный обработчик конфигурации или если многие файлы конфигурации могут редактировать другие люди, возможно вам подойдёт модуль Config::Auto. В общем, он берёт файл и соображает, как превратить его в хэш конфигурации. (Он может догадываться, ориентируясь на имя файла конфигурации). Использовать его просто (если это сработает):
#!/usr/bin/perl

use strict; use warnings;
use Config::Auto;

my $config = Config::Auto::parse("your.config");
...
Чем всё закончится - зависит от того, как выглядит файл конфигурации (внезапно). В случае файла вида переменная=значение получится то, что ожидается: то же самое, что получилось в первом примере. Можно указать файл конфигурации, который Config::Auto не сможет понять (страх и ненависть).

Настоящие хакеры используют модуль Parse::RecDescent

Если нужный вам файл сложен, положитесь на Parse::RecDescent. Он реализует умный разбор файла сверху вниз. Чтобы им воспользоваться, нужно указать грамматику. (Вы ведь её помните, не так ли? Если нет, читайте дальше.) Модуль создаёт обработчик по вашей грамматике. Вы скармливаете текст обработчику, а он выполняет действия, указанные в описании грамматики.

Чтобы понять, как он работает, попробуем разобрать римские числа. Программа ниже считывает числа с клавиатуры и преобразует их из римских в десятичные, так что XXIX превращается 29.
#!/usr/bin/perl

use strict; use warnings;
use Parse::RecDescent;

my $grammar = q{
  Numeral : TenList FiveList OneList /\Z/
              { $item[1] + $item[2] + $item[3]; }
          | /quit/i { exit(0); }
          | 

  TenList : Ten(3) { 30 }
          | Ten(2) OptionalNine { 20 + $item[2] }
          | Ten OptionalNine { 10 + $item[2] }
          | OptionalNine { $item[1] }

  OptionalNine : One Ten { 9 }
               | { 0 }

  FiveList : One Five { 4 }
           | Five { 5 }
           | { 0 }

  OneList : /(I{0,3})/i { length $1 }

  Ten : /X/i

  Five : /V/i

  One : /I/i
};

my $parse = new Parse::RecDescent($grammar);

while (<>) { chomp; my $value = $parse->Numeral($_); print ``Значение: $value\n''; }
Как вы успели заметить, большую часть программы составляет описание грамматики. Оставшаяся часть очень проста. В ней я просто получил обработчик от конструктора Parse::RecDescent, и дальше просто вызываю метод Numeral в цикле.

Как понять эту грамматику? Начнём сначала. Грамматика состоит из правил. Правило для Numeral (римских чисел) говорит:
Numeral (римские числа) образованы одним из вариантов:
  TenList (список десяток), затем FiveList (список пятёрок) и, наконец, из OneList (списка единиц)
  ИЛИ
  слово quit (не является римским числом, но позволяет завершить разбор)
  ИЛИ
  что-то другое, что воспринимается как ошибка.
Итак, видно что сначала идёт TenList и компания. Код после первого варианта называется действием. Если правило соответствует возможности, оно производит действие для этой возможности. Поэтому если найдено правильное Numeral (римское число), выполняется действие. Это отдельное действие складывает значения, накопленные TenList, FiveList и OneList. Элементы нумеруются начиная с 1, поэтому значение TenList располагается в $item[1] и т.д.

Как получается значение TenList? Итак, если начало Numeral подходит, идёт поиск первого действительного TenList. Есть четыре возможности:
TenList соответствует одному из следующих вариантов:
  три Tens (три десятки)
  ИЛИ
  два Tens (две десятки), затем OptionalNine (необязательная девятка)
  ИЛИ
  Ten (десятка), затем OptionalNine
  ИЛИ
  OptionalNine
Эти варианты сопоставляются по порядку. Ten (десятка) - это просто буква X в верхнем или нижнем регистре (смотрите правило Ten). Результат действия - это результат его последнего выражения. Так что, если обнаружено три десятки, TenList вернёт 30. Если обнаружено две десятки, TenList вернёт 20 плюс то, что вернёт OptionalNine.

Римское число IX - это наше 9. Я назвал его OptionalNine (необязательная девятка). (Имена могут быть совершенно произвольными.) Итак, после нуля, одного или двух X'ов может идти IX, которое добавит к результату 9. Если IX отсутствует, OptionalNine совпадёт с пустым правилом. Это правило не забирает поступающий текст и возвращает ноль.

Римские числа намного более сложны, чем грамматика, которую я обрабатываю. Для начала, по моему календарю сейчас MMIII год. В этой грамматике нет M. К тому же, некоторые римляне думали, что число IIIIII совершенно правильное. В моей грамматике три - это предельное количество повторов и повторяться могут только I и X. Кроме того, вычитание может отнимать только единицу. Поэтому IIX - не восемь, это не правильное число. Эта грамматика может распознать любое нормализованное римское число вплоть до 38. Вы вольны дополнить грамматику.

Parse::RecDescent не настолько быстр, как синтаксический анализатор, сгенерированный yacc, но им проще пользоваться. Обратитесь к документации в составе дистрибутива за более подробной информацией, особенно к учебнику, который изначально появился в The Perl Journal.

Если посмотреть, что находится внутри синтаксического анализатора (например, при помощи Data::Dumper), можно решить, что он на самом деле реализует шаблон Интерпретатор. Всё-таки он создаёт из грамматики дерево объектов. Приглядитесь и заметите ключевые отличия. Все объекты дерева являются членами классов, подобных Parse::RecDescent::Action, которые были написаны Дэмианом Конуеем (Damian Conway), когда он писал модуль. По представлениям банды четырёх, шаблон Интерпретатор должен создавать класс для каждой неоконечной грамматики (в примере выше классами стали бы Numeral, ReducedTen и т.д.). Однако, типы узлов дерева отличаются для каждой грамматики.

Это отличие заключается в двух предположениях: (1) это делает генератор синтаксических анализаторов RecDescent проще и (2) его результат быстрее.

Итог

В этой части мы познакомились с использованием ссылок на код для реализации шаблонов Стратегия и Шаблонный метод. Мы даже рассмотрели, как поместить код в чужой класс. Строитель превращает текст во внутреннюю структуру данных, чем и занимается большинство Интерпретаторов. Эти структуры часто могут быть просто сочетаниями хэшей, списков и скаляров. Если вам нужна простота чтения, воспользуйтесь split или Config::Auto. Если нужно что-то сложнее, воспользуйтесь Parse::RecDescent. Если он работает недостаточно быстро, может потребоваться один из генераторов синтаксических анализаторов, подобных yacc.

В следующий раз мы рассмотрим шаблоны, которые действительно относятся к объектам.

воскресенье, 10 ноября 2013 г.

Шаблоны проектирования в Perl

Перевод статьи: Perl Design Patterns
Автор: Фил Кроу (Phil Crow)

Введение

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

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

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

Перед реализацией объектов необходимо понимать основы объектной системы Perl. Вы можете изучить её в печатных источниках, например, в Книге рецептов Perl Тома Кристиансена (Tom Christiansen) и Нэта Торкингтона (Nat Torkington) или по книге Объектно-ориентированный Perl Дэмиана Конуэя (Damian Conway). Но простейший способ изучить основы - это perldoc perltoot.

В качестве обоснования моего подхода позвольте мне начать с небольшой объектно-ориентированной философии. Вот два моих основных принципа объектов:
  1. Объекты хороши, когда данные и методы тесно связаны.
  2. В большинстве других случаев объекты - это излишество.
Позвольте мне кратко обосновать эти принципы.

Объекты хороши, когда данные и методы тесно связаны.

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

В большинстве других случаев объекты - это излишество.

Рассмотрим несколько примеров из других языков. В Java есть класс java.lang.Math. Он содержит, например, функции sine и cosine. Он содержит только методы класса и пару констант класса. Их не стоило насильно вписывать в объектно-ориентированные рамки, поскольку не существует объектов Math. Лучше было бы поместить эти функции в ядро, полностью удалив или сделать простые не объектно-ориентированные функции. Последняя возможность в Java недоступна в принципе.

Или обратимся к стандартной библиотеке шаблонов C++. Для того, чтобы сделать C++ обратно совместимым с C и сохранить возможность строгой статической проверки типов, необходим целая система шаблонов. То, что должно являться частью самого языкового ядра, существует в виде неуклюжих объектно-ориентированных конструкций. Почему бы в язык для начала просто не ввести более удобный тип для массива? Затем можно было бы заняться такими общеизвестными структурами данных, как стеки, очереди, двусвязные очереди и многими другими структурами данных, которые изучают в школе.

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

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

Итератор

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

Банда четырёх предполагает использование описанного выше приёма: превратить концепцию в объект. Это означает, что вы должны сделать итератор объектом. Каждый класс объекта, который нужно обходить должен иметь метод, возвращающий объект-итератор. Этот объект должен быть единообразным. Например, рассмотрим следующий код, в котором итератор используется для обхода ключей хэша в Java.
for (Iterator iter = hash.keySet().iterator(); iter.hasNext();) {
    Object key = iter.next();
    Object value = hash.get(key);
    System.out.println(key + "\t" + value);
}
Объект HashMap содержит элементы, которые можно обойти - это его ключи. Вы можете запросить их с помощью метода, возвращающего множество ключей - keySet. Это множество предоставляет вам объект-итератор Iterator, если вызвать его метод iterator. Iterator отвечает на запрос hasNext истиной, если ещё остались элементы для обхода и ложью, если элементы закончились. Метод next возвращает следующий объект из последовательности, которой управляет Iterator. С помощью этого объекта key, HashMap возвращает следующее значение в ответ на вызов get(key). Это аккуратно и чисто в полностью объектно-ориентированном языке с ограниченными операторами и встроенными типами. И это точный пример шаблона Итератор от банды четырёх.

В Perl любой встроенный или пользовательский объект, который можно обойти, имеет метод, возвращающий упорядоченный список элементов для обхода. Чтобы обойти список, его нужно просто поместить внутрь скобок цикла foreach. Поэтому пример выше для обхода хэша в Perl будет выглядеть следующим образом:
foreach my $key (keys %hash) {
    print "$key\t$hash{$key}\n";
}
Я мог бы реализовать этот шаблон в точном соответствии с диаграммой банды четырёх, но в Perl для этого есть более подходящий способ. В Perl 6 появится возможность вернуть "ленивый" список, элементы которого будут вычисляться по мере необходимости, так что приведённый выше код станет более эффективным, чем сейчас. В Perl 5 при вызове keys список ключей создаётся целиком. В будущем список ключей будет создаваться при необходимости, в большинстве случаев экономя память и время, если цикл закончился досрочно.*

Встроенная поддержка итераций в ядре языка свидетельствует о качестве дизайна Perl. Вместо неуклюжего механизма без поддержки ядром языка, как в Java и C++ (который, однако, есть в стандартной библиотеке шаблонов), Perl содержит этот шаблон в ядре языка. Как было сказано во введении, это принцип Perl:

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

Пример выше использует ядро языка. Чтобы убедиться, что foreach полностью реализует шаблон итератор, даже для пользовательских модулей, рассмотрим пример из CPAN: XML::DOM. DOM для XML был определён Java-программистами. Один из методов, который вы можете вызвать для обработки DOM-документа, это getElementsByTagName. В спецификации DOM он возвращает NodeList, являющийся коллекцией Java. Однако, NodeList работает подобно Set в Java-коде из примера выше. Вы должны попросить его вернуть Итератор, а затем обойти его.

Если Perl-программист реализует DOM, он безусловно сделает так, чтобы getElementsByTagName возвращал обычный список Perl. Чтобы обойти список, он скажет что-то вроде:
foreach my $element ($doc->getElementsByTagName("tag")) {
    # ... обработать элемент ...
}
Это разительно отличается от излишне многословного варианта Java:
NodeList elements = doc.getElementsByTagName("tag");
for (Iterator iter = elements.iterator(); iter.hasNext();) {
    Element element = (Element)iter.next();
    // ... обработать элемент ...
}
Одно из достоинств Perl заключается в возможности эффективно совмещать процедурный и объектно-ориентированный подходы с использованием возможностей ядра. На самом деле то, что банда четырёх предлагает реализовывать шаблон при помощи объектов и эти объекты нужны только в языках вроде Java, совершенно не означает, что программисты на Perl должны игнорировать не объектные возможности Perl.

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

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

Декоратор

При обычной работе декоратор оборачивает объект, реализуя тот же интерфейс, что и обёрнутый объект. Например, предположим я добавил декоратор сжатия к объекту для записи файлов. Вызывающий код передаёт объект для записи файла конструктору декоратора и осуществляет запись в декоратор. Метод записи из декоратора сначала сжимает данные, а затем вызывает метод записи из обёрнутого объекта, осуществляющего запись. Любой другой тип объекта для записи файлов может быть обёрнут тем же декоратором, если все объекты для записи файлов реализуют один и тот же интерфейс. Декораторы можно выстраивать в цепочку. Текст может быть преобразован из ASCII в Unicode одним декоратором, а сжат другим. Порядок декораторов в цепочке имеет значение.

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

Ввод-вывод - наиболее частое применение декораторов. Perl позволяет реализовать декорацию ввода-вывода напрямую. Рассмотрим пример, приведённый выше: сжатие в процессе записи. Есть два способа сделать это.

Использование оболочки и её инструментов

При открытии файла на запись в Perl можно воспользоваться средствами декорации из оболочки. Вот код примера рассмотренного выше:
open FILE, "| gzip > output.gz"
    or die "Не могу открыть gzip и/или output.gz: $!\n";
Теперь всё, что я пишу, пропускается через gzip перед тем, как попасть в output.gz. Этот способ пригоден до тех пор (1), пока вам хочется использовать оболочку, что иногда может оказаться не безопасным; и (2) в оболочке есть инструменты для того, что вам нужно сделать. Также внушает беспокойство вопрос об эффективности. Операционная система порождает новый процесс на шаге gzip. Создание процесса - это одна из самых медленных операций операционной системы, не являющейся операцией ввода-вывода.

Связывание (tying)

Если нужно больше контроля над процессом обработки данных, тогда можно задекорировать его самостоятельно, при помощи механизма Perl под названием tie. Он быстрее, проще в использовании и мощнее в Perl 6, но работает и в Perl 5. Он работает с использованием объектно-ориентированной системы Perl; обратитесь к странице perltie за более подробной информацией.

Предположим, что нужно предварить отметкой времени каждую строку, выводимую в дескриптор. Вот связанный (tied) класс, который это делает.
package AddStamp;
use strict; use warnings;

sub TIEHANDLE {
    my $class = shift;
    my $handle = shift;
    return bless \$handle, $class;
}

sub PRINT {
    my $handle = shift;
    my $stamp = localtime();
    print $handle "$stamp ", @_;
}

sub CLOSE {
    my $self = shift;
    close $self;
}

1;
Это минимальный класс, в действительности может понадобиться дополнительный код, чтобы сделать декоратор более завершённым. Например, в коде выше не осуществляется проверка возможности записи в дескриптор, не проверяется, что он реализует PRINTF, поэтому вызов printf может завершиться ошибкой. Не стесняйтесь подумать о деталях. (Опять же, обратитесь к perldoc perltie за более подробной информацией.)

Вот несколько вещей, которые необходимо сделать. Конструктор связанного (tied) класса дескриптора файла называется TIEHANDLE. Его имя фиксировано и состоит из букв верхнего регистра, потому что Perl будет его вызывать. Это метод класса, поэтому его первый аргумент - имя класса. Другой аргумент - дескриптор, открытый на запись. Конструктор просто "благословляет" (bless) ссылку на дескриптор и возвращает её.

Метод PRINT принимает объект, сконструированный TIEHANDLE, и все аргументы, которые нужно напечатать. Он подсчитывает отметку времени и отправляет их вместе с исходными аргументами в дескриптор, используя настоящую функцию печати. Именно так работает декоратор. Декорированный объект позволяет печатать точно так же, как обычный дескриптор. Он выполняет небольшую работу, затем вызывает такой же метод из обёрнутого объекта.

Метод CLOSE закрывает дескриптор. Я использую наследование от Tie::StdHandle, чтобы получить этот метод и многие другие.

Поскольку я поместил AddTimeStamp.pm в каталог, входящий в последовательность поиска библиотек, я могу воспользоваться им таким образом:
#!/usr/bin/perl

use strict; use warnings;
use AddStamp;

open LOG, ">output.tmp" or die "Не могу записать output.tmp: $!\n";
tie *STAMPED_LOG, "AddStamp", *LOG;

while (<>) {
    print STAMPED_LOG;
}

close STAMPED_LOG;
После обычного открытия файла на запись я использую встроенную функцию для связывания дескриптора LOG с классом AddStamp под именем STAMPED_LOG. После этого я обращаюсь исключительно к STAMPED_LOG.

Если есть другой декоратор связывания, тогда можно передать связанный дескриптор ему. Единственный недостаток заключается в том, что Perl 5 работает со связанными объектами медленнее, чем с обычными. Ещё, по моему опыту, диски и сеть - это наиболее частые узкие места, поэтому не стоит обращать внимание на неэффективность использования памяти. В любом случае, если я ускорю скрипт на 90 процентов, я не сэкономлю много времени, потому что скрипт изначально работал не долго.

Такой подход работает для многих встроенных типов: скаляров, массивов, хэшей, как и для дескрипторов файлов. В документе perltie объясняется, как применять связывание к каждому из них.

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

Декорирование списков

Одна из наиболее частых задач в Perl - это какое-либо преобразование списков. Возможно понадобится пропустить все элементы списка, начинающиеся с подчёркивания. Возможно потребуется отсортировать список или обратить порядок его элементов. Многие встроенные функции являются фильтрами списков. Они похожи на фильтры Unix, которые ожидают строки данных на стандартном потоке ввода, которые они каким-то образом преобразуют, перед тем как отправить результат на стандартный поток вывода. Как и в Unix, фильтры списков в Perl могут быть объединены в цепочку. Например, предположим, что нужен список всех подкаталогов в текущем каталоге в обратном алфавитном порядке. Вот одно из возможных решений:
#!/usr/bin/perl

use strict; use warnings;

opendir DIR, ".",
    or die "Не могу прочитать этот каталог, как мы сюда попали?\n";
my @files = reverse sort map { -d $_ ? $_ : () } readdir DIR;
closedir DIR;
print "@files\n";
В Perl 6 появился более понятный способ записи для этих операций, но при небольшом усилии можно научиться читать их и в Perl 5. Одна из интересных строк - шестая. Начните читать её справа (пользователи Unix воспримут это как обратный порядок). Сначала происходит чтение каталога. Поскольку map ожидает получить список, readdir возвращает список всех файлов в каталоге. map создаёт список с именем каждого файла, который является каталогом (или undef, если проверка -d не пройдена). sort упорядочивает список в порядке, подобном алфавитному, но использует вместо алфавита таблицу ASCII-кодов. reverse переставляет элементы списка в обратном порядке. Результат сохраняется в @files для последующей печати.

Можно очень легко создать собственный фильтр списков. Предположим, что нужно заменить уродливое использование map в примере выше (я считаю, что map всегда уродлив) на функцию специального назначения, вот так:
#!/usr/bin/perl

use strict; use warnings;

sub dirs_only (@) {
    my @retval;
    foreach my $entry (@_) {
        push @retval, $entry if (-d $entry);
    }
    return @retval;
}

opendir DIR, "."
    or die "Не могу прочитать этот каталог, как мы сюда попали?\n";
my @files = reverse sort { lc($a) cmp lc($b) } dirs_only readdir DIR;
closedir DIR;
local $" = ";";
print "@files\n";
Новая подпрограмма dirs_only заменяет map из предыдущего примера, пропуская нежелательные элементы списка.

Теперь в sort явным образом прописана подпрограмма сравнения. Это позволяет избежать ошибочной мысли о том, что dirs_only - это подпрограмма сравнения. Поскольку я вписал сравнение, я решил извлечь пользу из сложившейся ситуации и сортирую более точно, игнорируя регистр символов.

Можно создавать любые фильтры списков, которые придутся вам по душе.

Теперь я покажу наиболее значимые типы декораций. Любые другие нужно будет реализовывать способом, обычным для банды четырёх.

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

Приспособленец

Мысль о повторном использовании объектов - это сущность шаблона "приспособленец". Благодаря Марку-Джейсону Доминусу (Mark-Jason Dominus), Perl заходит в этой идее дальше, чем предполагает банда четырёх. Кроме того, он сделал эту работу один раз и для всех. Ларри Уоллу (Larry Wall) нравится эта идея настолько сильно, что он продвигает её в ядро Perl 6 (речь идёт о продвижении самой концепции).

Что я хочу:

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

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

Вот пример того, как это работает в Perl. Предположим, что я хочу предоставить класс die для игр вроде Монополии или Костей. Мой класс die может выглядеть следующим образом: (Предупреждение: Это надуманный пример, имеющий целью проиллюстрировать приём.)
package CheapDie;
use strict; use warnings;
use Memoize;

memoize('new');

sub new {
    my $class = shift;
    my $sides = shift;
    return bless \$sides, $class;
}

sub roll {
    my $sides = shift;
    my $random_number = rand;
    return int ($random_number * $sides) + 1;
}

1;
На первый взгляд, этот класс похож на множество других классов. Он имеет конструктор под именем new. Конструктор сохраняет принятое значение в лексической переменной подпрограммы (так же известной как my-переменная), возвращая "благославлённую" ссылку на неё. Метод roll генерирует случайное число, умножает его на количество граней и возвращает результат.

Всё удивительное заключается в этих двух строчках:
use Memoize;
memoize('new');
Они используют выдающиеся возможности Perl. Вызов функции memoize изменяет таблицу функций вызывающего модуля таким образом, что new оказывается обёрнутой. Обёртка функции проверяет входящие аргументы (в данном случае - количество граней). Если обёртка до этого не встречала таких аргументов, она вызовет функцию, запрошенную пользователем, сохранит результат в кэш и вернёт его пользователю. На это потребуется больше времени и памяти, чем если бы этот модуль не использовался.

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

Единственное, о чём следует помнить - о том, что некоторые методы не выигрывают от применения этого приёма. Например, если вызвать memoize для roll, то она каждый раз будет возвращать одно и то же число, что не будет ожидаемым результатом.

Отметим, что Memoize может использоваться в случаях, где может и не быть объектов - на самом деле в его документации не предусматривается его использование в качестве фабрик объектов.

Мало того, что в языках подобных Java нет встроенных функций для кэширования результатов вызова методов, они вообще не позволяют талантливым пользователям реализовать такие функции. Марк-Джейсон Доминус (Mark-Jason Dominus) великолепно с этим справился, реализовав Memoize, но Ларри Уолл (Larry Wall) сделал большее, дав ему такую возможность. Представьте, что Java позволяет пользователю написать класс, манипулирующий таблицей символов вызывающего модуля во время работы. Мне кажется, что я уже слышу вопли ужаса. Конечно, такие приёмы могут привести к злоупотреблениям, но мы бы потеряли не много, поскольку мы всегда можем отказаться от дурного кода менее талантливых программистов, которые ошибаются при редактировании таблицы символов.

В Perl такие вещи вполне законны, но некоторые из них лучше оставить для модулей с сильным сообществом разработчиков. Это позволяет обычным пользователям получать преимущества от волшебства, не беспокоясь о том, чтобы заставить работать собственное волшебство. Memoize - это образец. Вместо создания собственных обёрток для вызовов и схемы кэширования, воспользуйтесь хорошо протестированной обёрткой, идущей в комплекте с Perl (и посмотрите свойство 'is cached' - кэшируется, чтобы сделать подобное с подпрограммами в Perl 6).

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

Одиночка

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

В большинстве случаев можно просто воспользоваться Memoize. Мне это кажется наиболее разумным. (Смотрите выше раздел "приспособленец".) В этом случае каждый, кто хочет получить доступ к ресурсу, вызывает конструктор. Первое же обращение к конструктору повлечёт за собой создание объекта, который и будет возвращён. Последующие вызовы конструктора будут возвращать объект, созданный при первом вызове конструктора.

Есть много других способов достичь того же результата. Например, если вы предполагаете, что вызывающий код может передать неожиданные аргументы, тогда Memoize будет создавать множество экземпляров, по одному на каждый набор аргументов. В этом случае есть смысл воспользоваться модулями управления одиночек, например, такими как Cache::FastMemoryCache на CPAN. Вы также можете воспользоваться переменными в области видимости файла, назначая им значение в блоке BEGIN. Помните, что bless не может использоваться в методе. Вы могли бы написать так:
package Name;

my $singleton;

BEGIN {
    $singleton = {
        attribute => 'value',
        another => 'something',
    };
    bless $singleton, "Name";
}

sub new {
    my $class = shift;
    return $singleton;
}
Этот пример демонстрирует прямолинейный способ, который позволяет избежать некоторых накладных расходов Memoize. В этом примере я не учитывал возможность создавать подклассы. Может быть я должен был бы это сделать, но шаблон утверждает, что одиночка всегда принадлежит лишь одному классу. Это основополагающее утверждение об одиночках:

"Может существовать только один одиночка."**

Итог

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

В следующий раз, если некоторые высокомерные объектно-ориентированные программисты заведут разговор о шаблонах, будьте уверены - вы знаете, как их использовать. Фактически, они встроены в ядро вашего языка (по крайней мере если подразумевать Perl).***

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

Примечания

Я написал эти статьи после прохождения курса банды четырёх от известной консалтинговой и тренинговой компании. Мои записки основаны на знаниях многих людей в сообществе Perl, включая Марка-Джейсона Доминуса (Mark-Jason Dominus), который продемонстрировал на YAPC 2002 свой талант обращения с итераторами в Perl. Хотя написанное тут принадлежит мне, меня вдохновили Доминус и многие другие члены сообщества Perl, в наибольшей степени - Ларри Уолл (Larry Wall), который встраивал шаблоны в сердце Perl в течение многих лет. Как раз за разом показывают эти шаблоны, Perl аккуратно и тщательно реализует принципы поощрения. Вместо добавления коллекций в виде исходных текстов модулей, как это сделано в Java и C++, в Perl есть только два вида коллекций: массивы и хэши. Оба входят в ядро языка. Я думаю, что величайшая сила Perl заключается в том, что сообщество может выбирать, что должно войти в ядро, а что должно быть вынесено из него. Perl 6 делает Perl лишь более конкурентноспособным в войне идей проектирования языков.****

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

*) Так называемые "ленивые" списки имеются в Python и используются довольно часто. Например, при чтении строк из файла. Или при обходе диапазона целых чисел при помощи xrange. Или при переборе строк, возвращённых запросом к базе данных. Автор излишне зациклен на Perl и превозносит его подход чуть ли не как идеальный. Но из всего перечисленного Perl по умолчанию умеет "лениво" перебирать только строки файлов. Впрочем, при необходимости, реализовать ленивость в Perl 5 всё же можно - для этого можно воспользоваться операцией связывания, tie.

**) Тут я не смог пройти мимо, чтобы не процитировать фразу из сериала "Компьютерщики": "Я - одинокий одиночка, идущий одинокой ночью по одинокой дороге. Один."

***) На самом деле в этой статье шаблоны рассмотрены довольно поверхностно. Для полного понимания лучше всё же прочитать книжку, хоть она и довольно тяжела для восприятия. Впрочем, пользу от статьи тоже не стоит недооценивать.

****) Статья была написана в 2003 году, в разгар работы над Perl 6, когда ещё казалось, что у него есть будущее.

воскресенье, 3 ноября 2013 г.

Подключение к MS SQL Server из Debian Wheezy

На работе однажды возникла задача написать программу на Python или Perl, которая должна работать с данными в базе MS SQL Server. Для начала, поскольку я не любитель GUI-клиентов к базам данных SQL, я решил найти подходящий консольный клиент.

1. Консольный клиент sqsh

Для подключения к базе данных попробовал воспользоваться консольным клиентом sqsh из одноимённого пакета. Позволяет подключаться к базам данных Sybase и MS SQL Server. Использует настройки драйвера FreeTDS, однако не использует саму библиотеку FreeTDS.

Установка клиента:
# apt-get install sqsh
Вместе с клиентом установятся библиотеки FreeTDS.

Настройка FreeTDS проста. В файл /etc/freetds/freetds.conf нужно добавить секцию для подключения к серверу MS SQL, задав адрес сервера, порт подключения, версию протокола TDS и кодировку клиента:
[server.domain.tld]
        host = server.domain.tld
        port = 1433
        tds version = 7.0
        client charset = UTF-8
Ключи клиента sqsh:
  • -S - имя или адрес сервера MS SQL,
  • -D - имя базы данных на сервере,
  • -U - имя пользователя для подключения к базе,
  • -P - пароль пользователя.
Пример подключения:
$ sqsh -S server.domain.tld -D database -U user -P password
Для выполнения запроса нужно сначала ввести его (можно в нескольких отдельных строках, нажимая Enter), а затем в новой строке указать команду go.

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

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

2. Консольный клиент sqlline на Java

Попробовал консольный клиент sqlline, написанный на Java. Для подключения к базам данных использует драйверы семейства JDBC, поэтому позволяет подключаться к любой базе данных, для которой имеется JDBC-драйвер. Для подключения к серверам Sybase и MS SQL Server используется драйвер jTDS.

Клиент и драйвер находятся в пакетах sqlline и libjtds-java. Установим их:
# apt-get install sqlline libjtds-java
Подключение к серверу базы данных не столь очевидно и осуществляется такой командой:
$ sqlline -u jdbc:jtds:sqlserver://server.domain.tld/database -n user -p password
Ключи клиента sqlline:
  • -u - URL базы данных, где server - IP-адрес или доменное имя сервера, а database - имя базы данных,
  • -n - имя пользователя для доступа к базе данных,
  • -p - пароль пользователя.
Как и в случае с sqsh, для выполнения запроса к базе данных, нужно сначала ввести сам запрос, а потом команду go.

Можно вводить любую команду, понимаемую сервером базы данных, либо одну из встроенных в клиент команд:
  • !quit - выйти из клиента,
  • !tables - показать список таблиц,
  • !table <таблица> - показать структуру указанной таблицы,
  • !columns <таблица> - показать колонки указанной таблицы.
Этот клиент оказался более удобным - форматирование таблиц осуществляется по ширине содержимого колонок. Кроме того, он включает в себя команды для просмотра структуры базы данных, которые позволяют обойтись без написания запросов к таблицам TABLES и COLUMNS в базе данных INFORMATION_SCHEMA. Из недостатков можно отметить, что команды для просмотра структуры таблиц не позволяют указать имя таблицы с пробелами в названии.

3. Python и модуль pymssql

Для доступа к MS SQL из Python попробовал воспользоваться модулем pymssql, который в Debian находится в пакете python-pymssql. Он тоже использует лишь файл конфигурации драйвера FreeTDS, однако подключается к серверу базы данных без использования самой библиотеки FreeTDS.

Установка драйвера:
# apt-get install python-pymssql
Тестовая программа:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import pymssql

db = pymssql.connect(host = 'server.domain.tld',
                     database = 'database',
                     user = 'user',
                     password = 'password',
                     charset = 'UTF-8')

query = db.cursor()
query.execute(u'''SELECT field1
                  FROM table1
                  WHERE field2 = 1
               ''')
s = '%s\n%d\n' % (pymssql.paramstyle, len(query.fetchall()))
for row in query.fetchall():
    s += u'%s\n' % row[0]
query.close()

db.close()

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

Нашёл заявку в багтрекере Debian: Debian Bug report logs - #709210: python-pymssql: Valid select queries return no results.

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

4. Python и модуль pyodbc

Решил попробовать ODBC-драйвер совместно с драйвером FreeTDS и модулем pyodbc. Для этого поставил пакеты из репозитория Debian: unixodbc, tdsodbc, odbcinst1debian2, python-pyodbc.

Установим необходимое:
# apt-get install unixodbc tdsodbc odbcinst1debian2 python-pyodbc
В отличие от модуля pymssql, для модуля pyodbc понадобится настроить не только FreeTDS, но и ODBC.

Настроил ODBC-драйвер TDS в соответствии с мануалом:
How to Configure Linux ODBC Connections for MS SQL или Connecting to a Microsoft SQL Server database from Python under Ubuntu.

Теперь нужно настроить ODBC-драйвер FreeTDS. Для этого в файл /etc/odbcinst.ini нужно вписать:
[FreeTDS]
Description     = TDS driver (Sybase/MS SQL)
Driver          = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so
Setup           = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so
CPTimeout       = 
CPReuse         =
Пути к динамическим библиотекам драйверов можно узнать, заглянув в список файлов в пакетах tdsodbc и odbcinst1debian2 соответственно:
$ dpkg -L tdsodbc
$ dpkg -L odbcinst1debian2
Сами эти пакеты можно найти, соответственно, по именам динамических библиотек (прямо как сепульки), следующими командами:
$ apt-file search libtdsodbc.so
$ apt-file search libtdsS.so
Далее нужно настроить DSN для доступа к интересующей нас базе данных. Добавим в файл /etc/odbc.ini описание DSN:
[server]
Driver = FreeTDS
Description = My cool MS SQL server
Trace = No
Server = server.domain.tld
Port = 1433
Database = database
В качестве имени секции используется произвольный текст, который и будет именем DSN'а.

В параметре Driver нужно указывать то значение, которое совпадает с названием секции в файле /etc/odbcinst.ini

В параметре Server нужно указывать то значение, которое совпадает с названием секции в файле /etc/freetds/freetds.conf

Если всё настроено правильно, тогда команда isql при указании этого DSN, имени пользователя и его пароля должна выводить следующий текст:
$ isql server user password
+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+
SQL> quit
Теперь время протестировать модуль pyodbc. Для этого я воспользовался несколько изменённой предыдущей тестовой программой:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import pyodbc

db = pyodbc.connect('DSN=server;UID=user;PWD=password')

query = db.cursor()
query.execute(u'''SELECT field1
                  FROM table1
                  WHERE field2 = 1
               ''')
s = '%s\n%d\n' % (pyodbc.paramstyle, len(query.fetchall()))
for row in query.fetchall():
    s += u'%s\n' % row[0]
query.close()

db.close()

print s
При запуске она выдаёт следующую ошибку:
$ ./test2.py 
Traceback (most recent call last):
  File "./test2.py", line 13, in 
    ''')
pyodbc.ProgrammingError: ('42000', "[42000] [FreeTDS][SQL Server]Could not find stored procedure 'S'. (2812) (SQLExecDirectW)")
Как видно, она ругается на отсутствие какой-то хранимой процедуры с именем S.

Ссылки pyodbc Issue 110: Could not find stored procedure и UBUNTU+PYMSSQL+CYRILLIC указывают на наличие проблем с кодировкой. Но понять, в чём эта проблема заключается и как её устранить, мне не удалось.

5. Perl и модуль DBD::Sybase

Для сравнения попробовал написать программу на Perl с использованием модуля DBD::Sybase из пакета libdbd-sybase-perl.

Установка модуля:
# apt-get install libdbd-sybase-perl
Тестовая программа:
#!/usr/bin/perl

use warnings;
use strict;

use utf8;
use DBI qw(:sql_types);

my $dbh = DBI->connect("DBI:Sybase:server=server.domain.tld;database=database;charset=utf8", "user", "password")
  or die $!;
$dbh->{syb_enable_utf8} = 1;

my $sth = $dbh->prepare("SELECT field1
                         FROM table1
                         WHERE field2 = ?");
$sth->execute('1');
while (my ($field1) = $sth->fetchrow_array())
{
  print "$field1\n";
}
$sth->finish();

$dbh->disconnect();
Программа выдала ожидаемый результат. Понадобилось только задать кодировку подключения, задать у него флаг utf8 и передать в запрос параметр подходящего типа: строку вместо числа.

На самом деле, правда, флаг syb_enable_utf8 не работает. Исправлять не пробовал, потому что если данные не нужно обрабатывать на уровне символов, то это не имеет значения, поскольку на вывод в любом случае поступает строка utf8. Если же необходимо обрабатывать эти строки внутри Perl-программы, приходится вручную выставлять у строк флаг utf8 или, если не городить костыли, то исправить сам модуль.

Буду признателен, если кто-то сообщит о способах решения упомянутых мной проблем: соберёт работающий модуль pymssql в виде deb-пакета, решит проблему с кодировками в pyodbc или исправит работу флага mysql_enable_utf8.

Использованные материалы:
1. Debian Bug report logs - #709210: python-pymssql: Valid select queries return no results
2. How to Configure Linux ODBC Connections for MS SQL
3. Connecting to a Microsoft SQL Server database from Python under Ubuntu
4. pyodbc Issue 110: Could not find stored procedure
5. UBUNTU+PYMSSQL+CYRILLIC