Архитектурные ошибки при разработке Perl/mod_perl ОО-приложений
Подходы
Речь пойдёт о двух вариантах организации объектно-ориентированного кода. Оба подхода приводят к решению и оба встречаются в реальной жизни.
Итак, выражаясь формально, нам надо получить из данных a данные b=f(a). (Собственно, всё программирование сводится к преобразованию одних данных в другие.)
Подход — программирование на объектах
Мы можем создать объект-конвертер и пользоваться им. Пример на Perl:
$c = new F; # создали объект-конвертер
$a = $c->f($b); # конвертируем
Естественно этот объект-конвертер можно использовать многократно, но может иметь некие настройки, которые можно менять по мере необходимости (хотя, хорошо ли это? возможно лучше все настройки делать при конструировании? во многом ответ зависит от конкретной ситуации). Код может выглядеть как-то так:
$c = new F;
$c->set_mode(2); # включили режим 'два'
print $c->f('one'); # конвертируем 'one'
$c->set_mode(3); # включили режим 'три'
print $c->f('two'); # конвертируем 'two'
Этот подход может показаться избыточным. Ну зачем нам создавать лишнюю сущность ($c), если всё можно сделать и без неё, особенно в Perl? Отсюда рождаются другие подходы.
Подход — программирование на конструкторах
Строго говоря, проблему можно решить с помощью статического метода:
$b=F->f($a);
но тогда не понятно, за чем вообще нужны объекты? Если мы остаёмся в рамках ОО-парадигмы, то есть ещё один способ: сделать всё конструктором:
$b = new F($a);
Обратите внимание: теперь мы имеем конструктор, но он возвращает не объект; мы имеем класс F, но не можем получить экземпляры этого класса. Абсурд? Подождите, дочитайте хотя бы до «примеров» и увидите, как это всё используется в реальной жизни.
По сути, программирование на статических методах очень похоже на программирование на конструкторах, но второе выводит нас на «новый уровень абстракции», с которого можно шагнуть дальше.
На пример можно сделать (нужно ли? скоро увидим) конструктор, который создаёт «пустой» объект, а параметр $a задаётся неким методом:
$e = new F; # "пустой" объект
$e->set($a); # будем обрабатывать $a
$b = $e->result; # обработать
Мы, казалось бы, возвращаемся к первому методу. Объект $e можно использовать многократно, что демонстрирует следующий пример. А если методы возвращают сам объект, то можно ещё и применить короткую запись:
$e = new F;
print $e->set('one')->result;
print $e->set('two')->result;
Такая схема может показаться изящной и ни чем не уступающей первой схеме. Однако давайте попытаемся увидеть за деревьями лес. Что делает, в данном случае, конструктор? Ничего. Вся функциональность конструктора вынесена в метод set. Так-что мы по-прежнему имеем всё то же программирование на конструкторах. Фактически это программирование на статических методах, но сильно завуалированное и таящее в себе потенциальные опасности, речь о которых пойдёт ниже.
Сейчас же, давайте отдадим дань другим авторам.
Примеры
Примеров программирования на объекта очень много, но я хочу начать с программирования на конструкторах. (То есть со второй модели.)
Программирование handler'ов в mod_perl (программирование на конструкторах)
Типичный mod_perl handler выглядит так:
sub handler($$) :method {
my ($class, $r) = @_;
$r->content_type('text/plain');
$r->print('Hello!');
return Apache2::Const::OK;
}
Как видите, по организации, это конструктор. Однако, по сути это статический метод, который обрабатывает объект $r (причём вызывает side-effect) и возвращает обычную константу. Никаких bless здесь нет и в помине.
Сервер вызывает этот метод как-то так:
$status = handler HandlerClass ($r)
Вам это не напоминает уже виденный фрагмент? Сравните с:
$b = new F($a);
Просто конструктор называется не new, а handler.
DBI (промежуточное решение)
Разработчики DBI предложили превосходное решение сложной (хотя и весьма распространённой) задачи.
Им нужно было естественным образом навязать программисту последовательность действий connect-prepare-execute и они прекрасно справились с задачей, введя дополнительные классы. Анатомия этих действий выглядит так:
# connect — просто статический метод
# $dbh — объект класса DBI::db
$dbh = DBI->connect(…)
# $sth — объект класса DBI::st
$sth = $dbh->prepare(…);
$rv = $sth->execute;
Я не назвал connect конструктором потому, что он создаёт не объект класса DBI, а объект класса DBI::db. То есть connect это статический метод класса DBI, вызывающий конструктор класса DBI::db.
Таким образом, пока вы не сделали connect у вас просто нет объекта, способного сделать prepare или execute. Пока вы не выполнили prepare у вас нет объекта, умеющего делать execute.
Но в этой схеме есть трещинки.
Чтобы получить данные вам снова нужен объект класса DBI::st. То есть вы можете выполнить fetchrow_array до execute, формально это дозволено, хотя смысла не имеет. В этом свете execute представляется «до-инициализатором», выполняющим часть работы конструктора, как в нашем примере:
$e = new F; # "пустой" объект
$e->set($a); # будем обрабатывать $a
$b = $e->result; # обработать
Есть и более мелкие трещинки в архитектуре DBI. На пример, метод bind_param, методы do и целая группа методов типа last_insert_id, которые идут в разрез со всей идеологией DBI.
Однако, DBI спроектирован хорошо, хоть и не во всём следует единой логике. Он уверенно занимает промежуточную позицию, в нём есть и программирование на объектах, не позволяющее допускать ошибок, и программирование на конструкторах.
SCGI (программирование на объектах)
Примеров чистого программирования на объектах очень много. Объектно-ориентированное программирование для того и создавалось, чтобы программировать на объектах. Вы легко найдёте массу таких примеров в Perl, а здесь я хотел бы привести пример на Python и вот почему.
SCGI — это аналог FastCGI. Идеология же очень близка и mod_perl'y (и mod_python'у, и любому mod_*). Приложение является сервером, оно стартует один раз, а потом обслуживает запросы. Программист под SCGI должен, точно так же как в mod_perl, написать свой обработчик запросов. Как это реализуется в mod_perl мы уже видели, давайте теперь посмотрим, как та же самая задача реализована в SCGI/Python.
Приложение выглядит так:
class MyAppHandler(scgi.scgi_server.SCGIHandler):
def produce(self, env, bodysize, input, output):
output.write('Сontent_type: text/plain\n\nHello!\n')scgi.scgi_server.SCGIServer(handler_class=MyAppHandler,
host='127.0.0.1',
port=3000,
max_children=3).serve()
Метод serve() класса scgi.scgi_server.SCGIServer запускает сервер. Сервер порождает объект (один) класса MyAppHandler и при каждом запросе вызывает метод produce этого класса, передавая ему все параметры запроса.
Строго говоря, мы получаем не один объект класса MyAppHandler, потому, что сервер может запускать несколько параллельных потоков обработки запросов. В моём примере таких потоков три. Но это уже детали на которых я не буду подробно останавливаться, хотя они тоже весьма существенны.
Обратите внимание, на сколько это похоже на наш код:
$c = new F; # при запуске сервера
$a = $c->f($b); # при каждом запросе
Одна и та же задача в mod_perl и в Python/scgi.scgi_server решена совершенно по-разному. Что значат эти отличия для разработчика?
Недостатки и достоинства
Давайте сравним два представленных подхода, стараясь двигаться от плохого к хорошему.
Зачем вообще нужны конструкторы при программировании на конструкторах?
При программировании на конструкторах, как вы уже видели, конструкторы в общем-то не нужны. Они являются статическими методами и чаще всего могут быть заменены простыми функциями без всякого ООП. ООП даёт только одно «преимущество» — возможность наследовать. Это позволяет переопределять и до-определять части конструктора, создавать классы-наследники и прочее. Но суть подхода всё равно остаётся не объектно-ориентированной.
Кроме того, всегда остаётся возможность и соблазн вынести часть работы конструктора в «до-инициализатор». Причём, при активном использовании наследования этот соблазн возрастает.
Что делает конструктор при «до-инициализации»?
При последовательном проведении в жизнь идеи «до-инициализации» объектов, конструкторы быстро вырождаются в пустышки типа:
sub new
{ my $this = shift; bless( {}, ref($this) || $this) }
Это цитата из реальной жизни.
Что делает этот конструктор? Что за объект он сконструировал? Как им пользоваться?
Любопытно, что вместо вызова такого конструктора:
$a = new SomeClass;
Можно было бы создать объект «на месте»:
$a = bless({}, 'SomeClass');
Это чуть длиннее, но избавляет от необходимости создавать отдельный файл для класса. То есть этот конструктор на столько не функционален, что его можно просто выкинуть и не заметить этого.
Но давайте не будем забывать про самый главный вопрос: как же пользоваться таким объектом?
В каком порядке вызывать «до-инициализаторы»?
Когда конструктор создаёт неполноценный объект — порождается самое большое зло. Работать с этим объектом можно только очень осторожно вызывая инициализаторы в строго определённом порядке.
Появляются такие персонажи:
XXX->new->lang( shift->_id )->template
или
my $i = YYY
->new
->select_offset (as_int( ($spg — 1) * $snm ))
->select_num (as_int($snm))
->select_sort (as_sort_col($sst))
->select_order (as_sort_ord($sso));
То, что «до-инициализаторы» требуют определённого порядка вызова — страшно само по себе. Но когда «до-инициализаторы» начинают вызываться в разных частях программы, код становится совершенно непостижимым.
Последней каплей становятся конструкции:
ZZZ->new->preinit->_handler($_[3])->postinit($_[2]->value)->_postinit2;
Вопрос: можно ли переставить местами postinit и _postinit2? Можно ли не вызывать _handler, ведь мы не вызвали __preinit? А что, есть ещё и __preinit? Так может его надо было вызывать?
Конфигурировать объект каждый раз?
Работа на конструкторах создаёт массу сложностей. Приведу только один пример. Допустим, при работе, ваш модуль использует некие конфигурационные параметры (это не редкость, не правда ли?).
Если вы используете mod_perl-подход, то у вас нет объекта, который жил бы на протяжении всей жизни сервера и мог бы один раз при инициализации считать конфигурацию и хранить её.
Вернее, вы можете создать такой объект самостоятельно в глобальном пространстве имён и использовать его. Но сама архитектура mod_perl не помогает вам это сделать. Кроме того, глобальные переменные это не очень хорошо.
Программируя на объектах (как было показано в примере SCGI/Python), вы получаете естественное место, где можно хранить конфигурацию сервера — ваш рабочий объект. Считывать же конфигурацию можно в конструкторе этого объекта. (На самом деле SCGI/Python предлагает специальный виртуальный метод, который вызывается конструктором, и множество других полезных методов.) Не нужны никакие глобальные переменные, всё просто и естественно.
Может быть всё же лучше программировать на объектах?
Подытожу. Чем хорошо программировать на объектах, а не на конструкторах.
• Это идеологически верно.
• Программирование на объектах автоматически дисциплинирует программиста (см. пример DBI).
• Объект конструируется один раз.
• Есть явное разграничение: это мы делаем при инициализации, а это при работе. Не нужны глобальные переменные для хранения конфигурации и прочего.
Хотя, конечно, есть ситуации, когда необходим объект-контейнер с методом «добавить в контейнер», это похоже на до-инициализацию. Строго говоря, чёткой грани между программированием на конструкторах и программированием на объектах нет. Однако программирование на конструкторах таит немалую опасность и увлекаться им не следует.