Елена Шишкина, ведущий программист Деньги Mail.Ru. Она покажет практический пример лени как двигателя прогресса в отдельно взятом веб-проекте:
- Надоело писать код? Будем думать, как его не писать!
- Боремся с однотипным кодом. Боремся с неоднотипным кодом.
- Код, которого не существует, и код, который существует.
- Следите за руками: программируем на конфигах!
- Как жить дальше?
3. С чего все начиналось
• Веб-сервис (JSON API)
– nginx
– Mojolicious
– PostgreSQL
– Вся логика в процедурах СУБД
• Архитектура веб-приложения
– Вертикальная нарезка на сервисы: auth, profile, contactlist, chat, …
– Горизонтальная нарезка
• www-layer
• Service layer
• Data layer
– Сервисы могут обращаться к друг другу через service layer
3
4. Типичная функция веб-слоя
• Проверка CSRF-токена
• Аутентификация
• Авторизация
• Чтение и валидация входных данных
• Обращение к сервисному слою
• Перехват и маппинг ошибок
• Генерация вывода
4
5. Функция веб-слоя
sub message {
my $self = shift;
my $result = eval {
my $form = $self->helper->read_form('chat/message');
die $form->export_errors if $form->has_errors;
die 'ERROR_CSRF_TOKEN'
unless $self->helper->token_ok($form);
die 'ERROR_NOT_AUTHORIZED'
unless $self->helper->check_auth($form);
$self->service->message($form->export);
};
unless ($result) {
my $err = $@;
$self->helper->logerr($err);
$result = $self->helper->map_error($err);
}
$self->render(json => $result);
}
5
6. Типичная функция сервисного слоя
• Обработка входящих данных
• Обращение к слою данных
• Сохранение в ленту активности пользователя
• Рассылка уведомлений
• Подготовка возвращаемых данных
–result: OK или код ошибки
–собственно данные:
• нет данных
• одно значение
• хэш
• массив хэшей
6
7. Функция сервисного слоя
sub message {
my ($self, $opts) = @_;
my $result = $self->data->message($opts);
$self->send_notify(chat_message => {
sender => $opts->{profile_id},
addressee => $result,
message => $opts->{message},
});
return $self->ok;
}
7
8. Типичная функция слоя данных
• Запрос данных из кэша (для статических запросов)
• Вызов процедуры СУБД
• Сохранение в кэш
• Инвалидация кэша
• Нормализация выходных данных
8
9. Функция слоя данных
sub contactlist {
my ($self, $opts) = @_;
my $cache_key = $self->to_cache_key(
'proifle.contactlist',
$opts
);
my $result = $self->cache->get($cache_key);
unless ($result) {
$result = $self->db->table(
'profiles.contactlist',
[ $opts->{profile_id} ]
);
$self->cache->set($cache_key, $result);
}
return $result;
}
9
10. Новая фича
• 1 процедура СУБД
• 3 копипасты с небольшими изменениями
В половине случаев меняются только названия,
ключи конфигов и имена процедур!
10
11. Можно как-нибудь так:
sub god_method {
my $self = shift;
my $cfg = $self->resolve;
my $result = eval {
my $form = $self->helper->read_form($cfg->{form});
die $form->export_errors if $form->has_errors;
if ($cfg->{check_token}) {
die 'ERROR_CSRF_TOKEN'
unless $self->helper->token_ok($form);
}
if ($cfg->{check_auth}) {
die 'ERROR_NOT_AUTHORIZED'
unless $self->helper->check_auth($form);
}
$self->service->god_method($cfg, $form->export);
};
unless ($result) {
my $err = $@;
$self->helper->logerr($err);
$result = $self->helper->map_error($err);
}
$self->render(json => $result);
}
Но это скучно!11
12. Будем генерировать методы на лету
• Не делаем лишних телодвижений: генерируем из
AUTOLOAD
• Чтобы не попросили странного, нам нужен список
разрешенных методов
• Генератор в базовом классе, списки методов – в
наследниках
• В наследниках можно описать вариации поведения
12
13. Проверка по списку разрешенных методов
sub _has_method {
my ($module, $method) = @_;
my $methods = ${ "$module::valid_methods" };
if (ref $methods && ref $methods eq 'HASH') {
return $methods->{$method};
} else {
return;
}
}
13
14. Метод-генератор
use Sub::Name;
sub _generate_sub {
my ($module, $method) = @_;
my $sub = subname "$module::$method", sub {
...
};
no strict 'refs';
*{"$module::$method"} = $sub;
return $sub;
}
14
15. AUTOLOAD
our $AUTOLOAD;
sub AUTOLOAD {
my $self = $_[0];
my ($method) = $AUTOLOAD =~ /^.+::(.*)$/;
my $package = blessed $self ? ref $self : $self;
return if !$method || $method eq 'DESTROY' || !
_has_method($package, $method);
my $sub = _generate_sub($package, $method);
goto ⊂
}
15
16. Oops! Mojolicious зовет can…
sub can {
my ($self, $method) = @_;
my $module = blessed $self ? ref $self : $self;
if (_has_method($module, $method)) {
return _generate_sub($module, $method);
} else {
return __PACKAGE__->SUPER::can($method);
}
}
16
17. Модули с фичами
use strict;
use warnings;
package MyProject::Controller::Chat;
#package MyProject::ServiceLayer::Chat;
#package MyProject::DataLayer::Chat;
use Mojo::Base 'MyProject::Controller::Base';
#use parent 'MyProject::ServiceLayer::Base';
#use parent 'MyProject::DataLayer::Base';
our $valid_methods = {
message => 1
};
1;
17
20. Функция веб-слоя
Всегда:
• читает и валидирует входные
параметры
• зовет сервисный слой
• перехватывает и маппит ошибки
• генерирует вывод
Может:
• читать определение веб-формы
из разных конфигов
• проверять CSRF-токен
• проверять аутентификацию
• проверять авторизацию
20
21. Определение метода для веб-слоя
use strict;
use warnings;
package
MyProject::Controller::Chat;
use Mojo::Base
'MyProject::Controller::Base';
our $valid_methods = {
message => {
check_token => 1,
check_auth => 1,
form => 'chat/message'
}
};
1;
Немного упростим
• Токен будем проверять по
умолчанию
• Аутентификацию тоже будем
проверять по умолчанию
• Имя формы = имя модуля + имя
метода
21
22. Определение метода для веб-слоя
use strict;
use warnings;
package MyProject::Controller::Chat;
use Mojo::Base 'MyProject::Controller::Base';
our $valid_methods = {
message => { }
};
1;
22
24. sub _generate_sub {
my ($module, $method) = @_;
my $def = dclone(_get_definition($module, $method) || {});
my $form_name = _form_name($module, $method, $def);
$def->{check_token} = 1 unless exists $def->{check_token};
$def->{check_auth} = 1 unless exists $def->{check_auth};
my $service_method = $def->{service_method} || $method;
my $sub = subname "$module::$method", sub {
my $self = shift;
my $result = eval {
my $form = $self->helper->read_form($form_name);
die $form->export_errors if $form->has_errors;
if ($def->{check_token} && !$self->helper->token_ok($form)) {
die 'ERROR_CSRF_TOKEN';
}
if ($def->{check_auth} && !$self->helper->check_auth($form)) {
die 'ERROR_NOT_AUTHORIZED';
}
$self->service->$service_method($form->export);
};
unless ($result) {
my $err = $@;
$self->helper->logerr($err); $result = $self->helper->map_error($err);
}
$self->render(json => $result);
};
no strict 'refs'; *{"$module::$method"} = $sub;
return $sub;
} 24
25. Функция сервисного слоя
Всегда:
• обращается к слою данных
• подготавливает возвращаемые
данные
Может:
• обрабатывать входящие данные
• сохранять данные в ленту
активности пользователя
• рассылать уведомления
• возвращать данные в разных
структурах:
– нет данных (только код
результата)
– одно значение
– хэш
– массив хэшей
25
26. Определение метода для сервисного слоя
use strict;
use warnings;
package MyProject::ServiceLayer::Chat;
use parent 'MyProject::ServiceLayer::Base';
our $valid_methods = {
message => {
returns => 'none',
notify => 1,
save_history => 0
}
};
1;
26
28. use Sub::Name;
use Storable qw(dclone);
sub _generate_sub {
my ($module, $method) = @_;
my $def = dclone(_get_definition($module, $method) || {});
my ($service) = $module =~ /^.+::(.*)$/;
my $data_method = $def->{data_method} || $method;
my $sub = subname "$module::$method", sub {
my ($self, $opts) = @_;
my $result = $self->service->$method($opts);
if ($def->{notify}) {
$self->send_notify((lc $service) . "_$method", $opts,
$result);
}
if ($def->{save_history}) {
$self->save_history((lc $service) . "_$method", $opts,
$result);
}
return $self->parse_answer($result, $def->{returns} || 'none');
};
no strict 'refs';
*{"$module::$method"} = $sub;
return $sub;
}
28
29. Функция слоя данных
Всегда:
• вызывает процедуру СУБД
Может:
• запрашивать данные из кэша
• передавать в процедуру разные
наборы параметров
• читать результат работы процедуры
в разном формате:
– нет возвращаемого значения
– одно значение
– строка
– таблица
• сохранять данные в кэш
• инвалидировать кэш
• нормализовывать выходные
данные
29
30. Определение метода для слоя данных
use strict;
use warnings;
package
MyProject::DataLayer::Chat;
use parent
'MyProject::DataLayer::Base';
our $valid_methods = {
message => {
args => [qw(profile_id
room_id reftime message)],
returns => 'table',
func => 'chat.message',
}
};
1;
Немного сократим
• Кэш по умолчанию не зовем и
не валидируем
• Имя процедуры базы строим по
шаблону:
– tablespace = имя модуля
– имя процедуры = имя метода
• Возвращаем по умолчанию
таблицу
30
32. use Sub::Name;
use Storable qw(dclone);
sub _generate_sub {
my ($module, $method) = @_;
my $def = dclone(_get_definition($module, $method) || {});
my ($service) = $module =~ /^.+::(.*)$/;
$service = lc $service;
my $db_func = $def->{func} || $service . '.' . $method;
my $layer_func = $def->{returns} || 'table';
$layer_func = 'exec' if $layer_func eq 'none';
my $sub = subname "$module::$method", sub {
my ($self, $opts) = @_;
my $cache_key = ($def->{use_cache} || $def->{invalidate_cache})
? $self->to_cache_key($service . '.' . $method, $opts)
: undef;
my $result = $def->{use_cache}
? $self->cache->get($cache_key)
: undef;
unless ($result) {
$result = $self->db->$layer_func($db_func, @$opts{@{ $def->{args} }});
$self->cache->set($cache_key, $result) if $def->{use_cache};
}
$self->cache->invalidate($cache_key, $opts) if $def->{invalidate_cache};
return $result;
};
no strict 'refs';
*{"$module::$method"} = $sub;
return $sub;
}
32
33. Чего мы добились
• Поигрались с кодогенерацией
• Убрали дублирование кода
• Формализовали декларацию данных для генерации
методов
Получилась отличная модель, но…
33
35. Гладко было на бумаге…
• После логина надо поставить куки
• После регистрации надо отправить email
• При добавлении фотографии нужно сохранить файл и
собрать метаинформацию
• При добавлении поста в ленту нужно распарсить и
обработать ссылки
• …
Нужен механизм для вызова произвольного кода!
35
36. Добавляем в определение метода коллбэки
prepare
• Вызывается до обращения к
нижележащему слою
• В качестве аргумента получает
входящие параметры метода
(для веб-слоя – объект формы)
• Может модифицировать
параметры (форму)
finish
• Вызывается после обращения к
нижележащему слою
• В качестве аргумента получает
данные, которые вернул
нижележащий слой
• Может модифицировать эти
данные
36
37. Для веб-слоя
our $valid_methods = {
method_name => {
prepare => sub {
my ($self, $form) = @_;
...
return $form;
},
finish => sub {
my ($self, $data) = @_;
...
return $data;
}
}
};
37
38. Чего мы добились
• Поигрались с кодогенерацией
• Убрали дублирование кода
• Формализовали декларацию данных для генерации
методов
• Научились добавлять вариативное поведение
Но нам все еще нужно добавлять по три файла,
в которых почти нет кода!
38
40. Избавляемся от файлов-модулей
• Собираем воедино разрозненные определения методов
• Выделяем инструмент – генератор модулей
• Выносим отдельно заполнение определения методов
значениями по умолчанию
• Добавляем в определение HTTP-метод запроса (GET, POST,
PUT, DELETE)
• Строим роутинг веб-фреймворка
40
41. Новое определение метода в сервисе
• URL запроса (по умолчанию – имя_сервиса/имя_метода
• Метод запроса (по умолчанию – GET)
• Параметры веб-слоя
• Параметры сервисного слоя
• Параметры слоя данных
41
42. Параметры веб-слоя
• Проверка токена (по умолчанию включена)
• Проверка аутентификации (по умолчанию включена)
• Имя формы (по умолчанию имя_сервиса/имя_метода)
• Коллбэки prepare и finish (по умолчанию отсутствуют)
• Имя метода сервисного слоя (по умолчанию то же самое)
42
43. Параметры сервисного слоя
• Имя метода слоя данных (по умолчанию то же самое)
• Отправка уведомлений (по умолчанию выключена)
• Сохранение в историю (по умолчанию выключена)
• Формат возвращаемых данных (по умолчанию
определяется слоем данных)
• Коллбэки prepare и finish (по умолчанию отсутствуют)
43
44. Параметры слоя данных
• Взятие данных из кэша (по умолчанию выключено)
• Инвалидация кэша (по умолчанию выключена)
• Имя процедуры СУБД (по умолчанию
имя_сервиса.имя_метода)
• Набор входящих аргументов процедуры СУБД
• Формат возвращаемых данных (по умолчанию – таблица)
44
45. Простейшее определение метода
my $services => {
chat => {
message => {
data_layer => {
args => [qw(profile_id room_id reftime
message)],
},
},
},
};
45
46. Генератор сервиса
• package MyProject::Core::ServiceGenerator;
sub init_service {
my ($self, $service_name, $definition) = @_;
$service_name = ucfirst $service_name;
no strict 'refs';
$definition = $self->normalize_definition($definition);
for my $layer (qw(Controller ServiceLayer DataLayer)) {
unshift @{*{ "MyProject::$layer::$service_name::ISA" }},
'MyProject::$layer::Base';
*{ "MyProject::$layer::$service_name::_get_definition" } =
sub {
return $definition;
};
}
my $method = lc($definition->{method});
$self->routes->$method($definition->{url})->to(
controller => $service_name,
action => $name
);
}
46
49. Добавляем в генератор сервиса проверку ISA
my $module = "MyProject::$layer::$service_name";
unless ($module->isa('MyProject::$layer::Base')) {
unshift @{*{ "$module::ISA" }}, 'MyProject::$layer::Base';
}
*{ "MyProject::$layer::$service_name::_get_definition" } = sub {
return $definition;
};
49
50. С AUTOLOAD все ОК, но can надо поправить
sub can {
my ($self, $method) = @_;
my $module = blessed $self ? ref $self : $self;
no strict 'refs';
if (my $sub = *{"$module::$method"}{CODE}) {
return $sub;
} elsif (_has_method($module, $method)) {
return _generate_sub($module, $method);
} else {
return __PACKAGE__->SUPER::can($method);
}
}
50
51. Чего мы добились
• Поигрались с кодогенерацией
• Убрали дублирование кода
• Формализовали декларацию данных для генерации методов
• Научились добавлять вариативное поведение
• Собрали определение сервисов и методов воедино
• Сделали определение типичных методов максимально
лаконичным
• Для новых и отлично ложащихся в шаблон сервисов мы даже
избавились от модулей
• Но при этом сохранили обратную совместимость
• А также возможность добавлять нестандартные методы
• Ленивая инициализация – при запуске сервера не генерируется
ничего лишнего
Но определение сервиса и метода –
это все еще код! 51
53. Декларациям место в текстовом формате
• Окончательно разделяем код и декларации
• Коллбэки prepare и finish выносим в модули, а в
декларациях оставляем имя модуля и функции
• Более компактный формат
• Легкое отключение сервиса на отдельных нодах: просто
удаляем конфиг!
53