воскресенье, 15 декабря 2013 г.

Установка и настройка Zabbix 2.2.0 в Debian Wheezy

Недавно вышел новый релиз Zabbix 2.2, в котором, по словам разработчиков, была существенна увеличена производительность, в 2-5 раз. Достигнуто это было, видимо, за счёт удаления столбцов lastvalue и prevvalue из таблицы items.

Кроме того, в настройках сервера теперь нельзя выключить Housekeeper - процессы Zabbix, удаляющие из базы данных устаревшие значения. Теперь эта настройка перенесена в веб-интерфейс, где можно выключить Housekeeper лишь для определённых видов данных, а не целиком. Это полезно, если настроено секционирование лишь части таблиц. При этом часть таблиц по-прежнему будут очищаться Housekeeper'ом, а часть будет очищаться настроенной в планировщике задач MySQL хранимой процедурой, которая будет удалять устаревшие секции таблиц.

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

Другим важным для меня изменением стало изменение настроек опроса по SNMP. Раньше использовались настройки опроса по умолчанию, из библиотеки SNMP. По умолчанию таймаут опроса составлял 1 секунду, а в случае неудачи делалось до 5 дополнительных попыток опроса. В Zabbix 2.2 для опроса по SNMP используется значение настройки Timeout, заданное в файле конфигурации и делается только одна попытка опроса.

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

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

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

1. Исправление исходников и сборка deb-пакетов

Добавим официальные репозитории с файлами для сборки пакетов в файл /etc/apt/sources.list:
deb-src http://repo.zabbix.com/zabbix/2.2/debian wheezy main
Установим ключ репозитория Zabbix и обновим список доступных пакетов:
# wget http://repo.zabbix.com/zabbix-official-repo.key -O - | apt-key add -
# apt-get update
Установим необходимые для сборки пакетов Zabbix зависимости и скачаем файлы для сборки пакетов:
# apt-get build-dep zabbix
# apt-get source zabbix
Ставим дополнительную сборочную зависимость, которую разработчики забыли прописать в файлы для сборки пакетов и пакет devscripts, из которого нам пригодится утилита dch:
# apt-get install unixodbc-dev
Переходим в каталоги zabbix-2.2.0, скачиваем и накладываем заплатку:
# cd zabbix-2.2.0
# wget http://stupin.su/files/zabbix220_snmp_timeout_retries.diff
# patch -Np0 < zabbix220_snmp_timeout_retries.diff
Текст самой заплатки:
diff -Naur src/zabbix_proxy/proxy.c new/zabbix_proxy/proxy.c
--- src/zabbix_proxy/proxy.c    2013-11-12 13:16:44.000000000 +0600
+++ new/zabbix_proxy/proxy.c    2013-12-07 13:29:39.283703310 +0600
@@ -42,6 +42,7 @@
 #include "housekeeper/housekeeper.h"
 #include "../zabbix_server/pinger/pinger.h"
 #include "../zabbix_server/poller/poller.h"
+#include "../zabbix_server/poller/checks_snmp.h"
 #include "../zabbix_server/poller/checks_ipmi.h"
 #include "../zabbix_server/trapper/trapper.h"
 #include "../zabbix_server/snmptrapper/snmptrapper.h"
@@ -407,6 +408,10 @@
 #endif
                {"Timeout",                     &CONFIG_TIMEOUT,                        TYPE_INT,
                        PARM_OPT,       1,                      30},
+               {"SNMPTimeout",                 &CONFIG_SNMP_TIMEOUT,                   TYPE_INT,
+                       PARM_OPT,       1,                      30},
+               {"SNMPRetries",                 &CONFIG_SNMP_RETRIES,                   TYPE_INT,
+                       PARM_OPT,       1,                      10},
                {"TrapperTimeout",              &CONFIG_TRAPPER_TIMEOUT,                TYPE_INT,
                        PARM_OPT,       1,                      300},
                {"UnreachablePeriod",           &CONFIG_UNREACHABLE_PERIOD,             TYPE_INT,
diff -Naur src/zabbix_server/poller/checks_snmp.c new/zabbix_server/poller/checks_snmp.c
--- src/zabbix_server/poller/checks_snmp.c      2013-11-12 13:16:49.000000000 +0600
+++ new/zabbix_server/poller/checks_snmp.c      2013-12-07 12:50:31.062073112 +0600
@@ -33,6 +33,9 @@
 }
 zbx_snmp_index_t;
 
+int CONFIG_SNMP_TIMEOUT;
+int CONFIG_SNMP_RETRIES;
+
 static zbx_snmp_index_t        *snmpidx = NULL;
 static int             snmpidx_count = 0, snmpidx_alloc = 16;
 
@@ -268,10 +271,10 @@
                        break;
        }
 
-       session.retries = 0;                            /* number of retries after failed attempt */
-                                                       /* (net-snmp default = 5) */
-       session.timeout = CONFIG_TIMEOUT * 1000 * 1000; /* timeout of one attempt in microseconds */
-                                                       /* (net-snmp default = 1 second) */
+       session.retries = CONFIG_SNMP_RETRIES - 1;              /* number of retries after failed attempt */
+                                                               /* (net-snmp default = 5) */
+       session.timeout = CONFIG_SNMP_TIMEOUT * 1000 * 1000;    /* timeout of one attempt in microseconds */
+                                                               /* (net-snmp default = 1 second) */
 
 #ifdef HAVE_IPV6
        if (SUCCEED != get_address_family(item->interface.addr, &family, err, MAX_STRING_LEN))
diff -Naur src/zabbix_server/poller/checks_snmp.h new/zabbix_server/poller/checks_snmp.h
--- src/zabbix_server/poller/checks_snmp.h      2013-11-12 13:16:48.000000000 +0600
+++ new/zabbix_server/poller/checks_snmp.h      2013-12-07 12:50:31.062073112 +0600
@@ -26,7 +26,8 @@
 #include "sysinfo.h"
 
 extern char    *CONFIG_SOURCE_IP;
-extern int     CONFIG_TIMEOUT;
+extern int     CONFIG_SNMP_TIMEOUT;
+extern int     CONFIG_SNMP_RETRIES;
 
 int    get_value_snmp(DC_ITEM *item, AGENT_RESULT *value);
 
diff -Naur src/zabbix_server/server.c new/zabbix_server/server.c
--- src/zabbix_server/server.c  2013-11-12 13:17:04.000000000 +0600
+++ new/zabbix_server/server.c  2013-12-07 13:29:22.605760996 +0600
@@ -43,6 +43,7 @@
 #include "housekeeper/housekeeper.h"
 #include "pinger/pinger.h"
 #include "poller/poller.h"
+#include "poller/checks_snmp.h"
 #include "poller/checks_ipmi.h"
 #include "timer/timer.h"
 #include "trapper/trapper.h"
@@ -360,6 +361,10 @@
 #endif
                {"Timeout",                     &CONFIG_TIMEOUT,                        TYPE_INT,
                        PARM_OPT,       1,                      30},
+               {"SNMPTimeout",                 &CONFIG_SNMP_TIMEOUT,                   TYPE_INT,
+                       PARM_OPT,       1,                      30},
+               {"SNMPRetries",                 &CONFIG_SNMP_RETRIES,                   TYPE_INT,
+                       PARM_OPT,       1,                      10},
                {"TrapperTimeout",              &CONFIG_TRAPPER_TIMEOUT,                TYPE_INT,
                        PARM_OPT,       1,                      300},
                {"UnreachablePeriod",           &CONFIG_UNREACHABLE_PERIOD,             TYPE_INT,
Если всё прошло успешно, то можно внести комментарии к изменениям. Запускаем команду для редактирования журнала изменений:
# dch -i
И приводим последнюю запись к подобному виду:
zabbix (1:2.2.0-2+stupin) UNRELEASED; urgency=low

  * Non-maintainer upload.
  * Added parameters SNMPTimeout and SNMPRetries to config of Zabbix-server
    and Zabbix-proxy

 -- Vladimir Stupin <wheel69@yandex.ru>  Sat, 07 Dec 2013 13:08:13 +0600
Теперь можно собрать deb-пакеты:
# dpkg-buildpackage -us -uc -b -rfakeroot
В каталоге выше появятся собранные пакеты, которые можно установить при помощи dpkg.

2. Установка Zabbix

Установим необходимые для работы Zabbix пакеты:
# apt-get install lighttpd php5-cgi snmp-mibs-downloader mysql-server mysql-client
После этого можно установить сам сервер Zabbix, веб-интерфейс, агент и утилиты. Если вы собирали пакеты самостоятельно, можете воспользоваться dpkg. Можно также воспользоваться моим репозиторием, в который я выложил собранные пакеты. Для этого нужно вписать в файл /etc/apt/sources.list строку:
deb http://deb.stupin.su/ wheezy main
И обновить список пакетов:
# apt-get update
Ставим Zabbix из пакетов:
# dpkg -i zabbix-server-mysql_2.2.0-2+stupin_amd64.deb zabbix-agent_2.2.0-2+stupin_amd64.deb zabbix-frontend-php_2.2.0-2+stupin_all.deb zabbix-get_2.2.0-2+stupin_amd64.deb zabbix-sender_2.2.0-2+stupin_amd64.deb
Или из репозитория:
# apt-get install zabbix-server-mysql zabbix-agent zabbix-frontend-php zabbix-get zabbix-sender
Zabbix-агент позволит наблюдать за самим компьютером, на котором мы устанавливаем Zabbix.

Утилита zabbix-get может пригодиться для отладки пользовательских параметров Zabbix-агента: скриптов, которые вызываются Zabbix-агентом при запросе этого параметра. Скрипт должен выводить результат на стандартный вывод, а Zabbix-агент вернёт выведенное значение Zabbix-серверу или утилите zabbix-get.

Утилита zabbix-sender может пригодиться для скриптов, запускаемых из планировщика задач. Эти скрипты могут отправлять результаты измерений на Zabbix-сервер при помощи утилиты zabbix-sender.

3. Настройка базы данных

В процессе установки Zabbix-сервера будет выведено предложение настроить базу данных. Если вы ставите Zabbix в первый раз, то можно согласиться с предложением и настроить новую базу. Если же у вас имеется база данных, стоит ответить на предложение отрицательно и настроить подключение к базе данных вручную, в файле /etc/zabbix/zabbix_server.conf. Диалог настройки выглядит следующим образом:



Можно те же самые действия проделать и вручную. Подключаемся консольным клиентом к СУБД и создаём базу данных для Zabbix:
$ mysql -uroot -p
> CREATE DATABASE zabbix DEFAULT CHARACTER SET UTF8 COLLATE UTF8_GENERAL_CI;
Создаём пользователя, от имени которого Zabbix будет подключаться к MySQL:
> INSERT INTO user(host, user, password) VALUES('localhost', 'zabbix', PASSWORD('zabbix_password'));
> FLUSH PRIVILEGES;
Даём пользователю zabbix полный доступ к базе данных zabbix:
> GRANT ALL ON zabbix.* TO 'zabbix'@'localhost';
> FLUSH PRIVILEGES;

Подключаемся к свежесозданной БД, создаём структуру БД и наполняем её данными:
> USE zabbix;
> SOURCE /usr/share/zabbix-server-mysq/schema.sql
> SOURCE /usr/share/zabbix-server-mysq/data.sql
> SOURCE /usr/share/zabbix-server-mysq/images.sql
Задаём пароль администратора системы мониторинга и выходим из MySQL:
> UPDATE users SET passwd=MD5('zabbix_admin_password') WHERE alias='admin';
> QUIT
4. Настройка SNMP

Удобно при опросе оборудования по SNMP в качестве OID'ов указывать не их числовое значение, а текстовое представление, взятое из соответствующего MIB-файла. Никто не запрещает использовать и другие MIB-файлы, но обычно мне бывает достаточно одного лишь IF-MIB'а. Файл IF-MIB скачивается в процессе установки пакета snmp-mibs-downloader, осталось лишь прописать его использование в файл конфигурации /etc/snmp/snmp.conf, вписав в него следующую строчку:
mibs +IF-MIB
5. Настройка сервера Zabbix

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

Отредактируем файл конфигурации /etc/zabbix/zabbix_server.conf, установив значения соответствующих опций. В случае если Zabbix-сервер и сервер MySQL находятся на одном компьютере, желательно использовать Unix-сокет:
DBHost=localhost
DBSocket=/var/run/mysqld/mysqld.sock
DBName=zabbix
DBUser=zabbix
DBPassword=zabbix_password

Другие полезные настройки, на которые следует обратить внимание:
ListenIP=127.0.0.1 

StartPollers=5
StartPollersUnreachable=1
StartPingers=1
Timeout=3

SNMPTimeout=1
SNMPRetries=3
Назначение настроек:
  • ListenIP указывает IP-адрес, на котором Zabbix-сервер будет принимать подключения от веб-интерфейса и утилиты zabbix-sender. Может оказаться проще оставить эту настройку закомментированной, ограничив допустимые адреса клиентов при помощи пакетного фильтра.
  • StartPollers задаёт количество процессов, занимающихся SNMP-проверками, проверками при помощи скриптов внешнего опроса и т.п. Если в меню веб-интерфейса "Администрирование" - "Очередь" не наблюдается отставания в опросе, то всё можно оставить как есть.
  • StartPollersUnreachable задаёт количество процессов, занимающихся опросом не отвечающего оборудования. Обычно стоит сделать запас на случай крупных аварий, когда неотвечающего оборудования становится очень много.
  • StartPingers - количество процессов, занимающихся опросом по ICMP. Для опроса используется утилита fping, для которой во временном каталоге формируются списки IP-адресов. Если опрашиваемых по ICMP узлов очень много, стоит увеличить эту настройку. Возможно также поможет использование в качестве временного каталога раздела tmpfs, т.к. при этом временные файлы будут создаваться в памяти. А может быть это и не поможет, потому что дисковый кэш на практике может нивелировать накладные расходы по записи и чтению этих файлов.
  • Timeout - тот самый период ожидания ответа, значение которого в моём случае не подходило для опроса по SNMP. Для того, чтобы скрипты внешнего опроса успевали отрабатывать, я обычно задаю значение 30 секунд.
  • SNMPTimeout и SNMPRetries - настройки, добавленные путём правки исходных текстов Zabbix. Задают период ожидания ответа по SNMP и количество попыток. Поскольку протокол SNMP работает поверх протокола UDP, который не гарантирует доставку, всё-же стоит делать несколько попыток опроса, а не прекращать опрос сразу после первой же неудачи. Я задал 1 секунду и 3 попытки соответственно. Считаю это оптимальными настройками.

По мере роста количества наблюдаемых узлов и элементов данных может потребоваться увеличивать размеры кэшей и выставить в /etc/sysctl.conf настройку, ограничивающую максимальной размер одного сегмента разделяемой памяти, увеличивая её по мере необходимости:
kernel.shmmax = 268435456
Изменить эту настройку в процессе работы системы можно запустив команду вида:
# sysctl kernel.shmmax=268435456
6. Настройка веб-сервера

Настроим Lighttpd для обслуживания веб-интерфейса Zabbix. Для этого создадим файл /etc/lighttpd/conf-available/20-zabbix.conf со следующим содержимым:
alias.url += (
  "/zabbix/" => "/usr/share/zabbix/"
)

$HTTP["url"] =~ "^/zabbix/conf/" {
  url.access-deny = ("")
}

$HTTP["url"] =~ "^/zabbix/api/" {
  url.access-deny = ("")
}

$HTTP["url"] =~ "^/zabbix/include/" {
  url.access-deny = ("")
}

$HTTP["url"] =~ "^/zabbix/include/classes/" {
  url.access-deny = ("")
}
Если хочется, чтобы веб-интерфейс Zabbix был доступен по адресу с собственным доменным именем, этот файл может выглядеть следующим образом:
$HTTP["host"] == "zabbix.domain.tld" {

  alias.url += (
    "/" => "/usr/share/zabbix/"
  )

  $HTTP["url"] =~ "^/conf/" {
    url.access-deny = ("")
  }

  $HTTP["url"] =~ "^/api/" {
    url.access-deny = ("")
  }

  $HTTP["url"] =~ "^/include/" {
    url.access-deny = ("")
  }

  $HTTP["url"] =~ "^/include/classes/" {
    url.access-deny = ("")
  }
}
Затем откорректируем настройки PHP, так чтобы он удовлетворял требованиям веб-интерфейса Zabbix. Для этого создадим файл /etc/php5/mods-available/zabbix.ini и пропишем в него следующие настройки:
max_execution_time=300
memory_limit=128M
post_max_size=16M
upload_max_filesize=2M
max_input_time=300
date.timezone=Asia/Yekaterinburg
Включим использование этого файла при обработке CGI-сценариев PHP:
# cd /etc/php5/cgi/
# ln -s ../mods-available/zabbix.ini conf.d/30-zabbix.ini
Осталось включить поддержку PHP в режиме FastCGI в веб-сервере, а также включить веб-интерфейс Zabbix и попросить веб-сервер применить новые настройки:
# lighty-enable-mod fastcgi-php
# lighty-enable-mod zabbix
# /etc/init.d/lighttpd force-reload
7. Настройка веб-интерфейса

Зайдём в веб-интерфейс Zabbix и проследуем по страницам мастера настроки веб-интерфейса:

Вместо использования мастера можно просто создать вручную файл conf/zabbix.conf.php со следующим содержанием:
<?php
// Zabbix GUI configuration file
global $DB;

$DB['TYPE']     = 'MYSQL';
$DB['SERVER']   = 'localhost';
$DB['PORT']     = '0';
$DB['DATABASE'] = 'zabbix';
$DB['USER']     = 'zabbix';
$DB['PASSWORD'] = 'zabbix_db_password';

// SCHEMA is relevant only for IBM_DB2 database
$DB['SCHEMA'] = '';

$ZBX_SERVER      = 'localhost';
$ZBX_SERVER_PORT = '10051';
$ZBX_SERVER_NAME = '';

$IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
?>
8. Настройка клиента точного времени

Настраивать точное время полезно всегда. В случае системы мониторинга это особенно полезно, поскольку происходящие в системе события и исторические данные имеют привязку ко времени. Для синхронизации системного времени я использую демон OpenNTPd, разработанный в рамках проекта OpenBSD. Он не перегружен функционалом и легко настраивается. Установим пакет openntpd:
# apt-get install openntpd
И пропишем в файл конфигурации /etc/openntpd/ntpd.conf опцию servers, которая использует в качестве серверов все IP-адреса, возвращаемые по доменному имени:
servers ntp.ufanet.ru
9. Настройка почтового релея

Для отправки уведомлений системы мониторинга на почту можно воспользоваться Postfix, настроив его в режиме простейшего почтового релея. Для этого можно воспользоваться одной из моих прошлых заметок: Простейший почтовый релей на основе Postfix

Заключение

Здесь рассмотрена лишь установка системы мониторинга. Её настройка - отдельная большая тема. Чтобы получить начальное представление об устройстве системы, рекомендую обратиться к статье: FreeBSD: Установка и начальная настройка системы мониторинга Zabbix. Более подробное описание можно найти на официальной вики-странице проекта: Руководство по Zabbix 2.2.

Дополнение от 11 июня 2014 года.

В патче были две грубейшие ошибки в настройках таймаутов и количества повторов SNMP, которые долгое время оставались незамеченными. Исправил патч.

воскресенье, 8 декабря 2013 г.

Настройка сервера VNC и RDP совместно с LightDM

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

Всё оказалось довольно просто: дисплейный менеджер LightDM умеет работать совместно с VNC-сервером, позволяя авторизоваться в системе и начать новый X-сеанс.

Установка и настройка VNC-сервера

Для начала устанавливаем пакет с VNC-сервером TightVNCServer:
# apt-get install tightvncserver
В файле конфигурации /etc/lightdm/lightdm.conf находим секцию VNCServer и приводим её к следующему виду:
[VNCServer]
enable=true
port=5900
width=1024
height=768
depth=8
Смысл всех настроек очевиден:
  • enable включает возможность подключиться к LightDM по протоколу VNC,
  • port задаёт номер TCP-порт, на котором VNC-сервер будет ожидать подключений,
  • width задаёт ширину экрана для VNC-сервера,
  • height задаёт высоту экрана для VNC-сервера,
  • depth задаёт глубину цвета для точки - 8 бит, 16 бит, 24 бита,
Осталось перезапустить дисплейный менеджер LightDM. Учтите, что при перезапуске LigthDM будут завершены все открытые X-сеансы, поэтому лучше завершить их вручную и выполнить следующую команду из текстовой консоли:
# /etc/init.d/lightdm restart
Теперь LightDM будет ожидать подключений на TCP-порту 5900 и при подключении клиента будет запускать VNC-сервер. Узнать, какой VNC-сервер будет запускаться, можно при помощи следующей команды:
# update-alternatives --list vncserver
Выбрать используемый VNC-сервер можно при помощи следующей команды:
# update-alternatives --config vncserver
Если в системе установлен только один VNC-сервер, будет использоваться он. При удалении используемого VNC-сервера система переключится на использование другого.

Лучше использовать клиент наиболее совместимый с сервером. В нашем случае - это TightVNCViewer из пакета xtightvncviewer. VNC-сервер одновременно будет выступать в роли X-сервера, который будет взаимодействовать с LightDM. При подключении мы увидим обычный экран LightDM с указанным разрешением и глубиной цвета, в котором можно ввести имя пользователя и пароль для создания нового X-сеанса.

Установка и настройка RDP-прокси

Для подключения к компьютеру с VNC-сервером по протоколу RDP можно установить и настроить специальный прокси-сервер XRDP, который принимает подключения по протоколу RDP, а сам устанавливает подключения по протоколу VNC к VNC-серверу (на самом деле в данном случае соединения будет принимать LightDM, запуская для их обслуживания VNC-сервер). Прокси будет одновременно выступать в роли RDP-сервера и VNC-клиента.

Установим пакет с прокси:
# apt-get install xrdp
Приведём файл конфигурации /etc/xrdp/xrdp.ini к следующему виду:
[globals]
bitmap_cache=yes
bitmap_compression=yes
port=3389
crypt_level=low
channel_code=1

[xrdp1]
name=default
lib=libvnc.so
username=
password=
ip=127.0.0.1
port=5900
На самом деле этот RDP-прокси может выступать и в роли RDP-клиента, может устанавливать подключения к произвольным компьютерам в сети, самостоятельно запускать X-серверы и использовать различные схемы аутентификации. В данном случае из файла конфигурации удалены все остальные варианты подключения и добавлен только один вариант с названием default, который позволяет подключиться к VNC-серверу, запущенному на том же компьютере, что и сам RDP-прокси. Подключение будет устанавливаться на TCP-порт 5900, а RDP-прокси не будет спрашивать у пользователя имя и пароль.

Теперь перезапустим прокси, чтобы настройки вступили в силу:
# /etc/init.d/xrdp restart
Теперь можно подключаться к VNC-серверу при помощи обычного RDP-клиента из Windows. При этом можно не думать об установке VNC-клиента, который не всегда может оказаться под рукой во "враждебной" среде Windows :)

Вот снимок экрана при подключении по VNC при помощи Remmina:

воскресенье, 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, когда ещё казалось, что у него есть будущее.