Пластилиновый код
Как перестать кодить и начать жить
Елена Шишкина,
ведущий программист
Деньги.Мэйл.Ру
Москва, 2015
1
ПОДУМАЕМ, КАК ЕГО НЕ ПИСАТЬ!
Надоело писать код?
2
С чего все начиналось
• Веб-сервис (JSON API)
– nginx
– Mojolicious
– PostgreSQL
– Вся логика в процедурах СУБД
• Архитектура веб-приложения
– Вертикальная нарезка на сервисы: auth, profile, contactlist, chat, …
– Горизонтальная нарезка
• www-layer
• Service layer
• Data layer
– Сервисы могут обращаться к друг другу через service layer
3
Типичная функция веб-слоя
• Проверка CSRF-токена
• Аутентификация
• Авторизация
• Чтение и валидация входных данных
• Обращение к сервисному слою
• Перехват и маппинг ошибок
• Генерация вывода
4
Функция веб-слоя
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
Типичная функция сервисного слоя
• Обработка входящих данных
• Обращение к слою данных
• Сохранение в ленту активности пользователя
• Рассылка уведомлений
• Подготовка возвращаемых данных
–result: OK или код ошибки
–собственно данные:
• нет данных
• одно значение
• хэш
• массив хэшей
6
Функция сервисного слоя
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
Функция слоя данных
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
Новая фича
• 1 процедура СУБД
• 3 копипасты с небольшими изменениями
В половине случаев меняются только названия,
ключи конфигов и имена процедур!
10
Можно как-нибудь так:
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
Будем генерировать методы на лету
• Не делаем лишних телодвижений: генерируем из
AUTOLOAD
• Чтобы не попросили странного, нам нужен список
разрешенных методов
• Генератор в базовом классе, списки методов – в
наследниках
• В наследниках можно описать вариации поведения
12
Проверка по списку разрешенных методов
sub _has_method {
my ($module, $method) = @_;
my $methods = ${ "$module::valid_methods" };
if (ref $methods && ref $methods eq 'HASH') {
return $methods->{$method};
} else {
return;
}
}
13
Метод-генератор
use Sub::Name;
sub _generate_sub {
my ($module, $method) = @_;
my $sub = subname "$module::$method", sub {
...
};
no strict 'refs';
*{"$module::$method"} = $sub;
return $sub;
}
14
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
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
Модули с фичами
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
Схема модулей
MyProject::Base
_has_method
AUTOLOAD
MyProject::Controller::Base
_generate_sub
MyProject::ServiceLayer::Base
_generate_sub
MyProject::DataLayer::Base
_generate_sub
MyProject::Controller::Chat
$valid_methods
MyProject::ServiceLayer::Chat
$valid_methods
MyProject::DataLayer::Chat
$valid_methods
can
18
БОРЕМСЯ С ОДНОТИПНЫМ КОДОМ
19
Функция веб-слоя
Всегда:
• читает и валидирует входные
параметры
• зовет сервисный слой
• перехватывает и маппит ошибки
• генерирует вывод
Может:
• читать определение веб-формы
из разных конфигов
• проверять CSRF-токен
• проверять аутентификацию
• проверять авторизацию
20
Определение метода для веб-слоя
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
Определение метода для веб-слоя
use strict;
use warnings;
package MyProject::Controller::Chat;
use Mojo::Base 'MyProject::Controller::Base';
our $valid_methods = {
message => { }
};
1;
22
Генератор для метода в веб-слое
23
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
Определение метода для сервисного слоя
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
Генератор метода для сервисного слоя
27
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
Определение метода для слоя данных
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
Генератор метода для слоя данных
31
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
БОРЕМСЯ С НЕОДНОТИПНЫМ КОДОМ
34
Гладко было на бумаге…
• После логина надо поставить куки
• После регистрации надо отправить email
• При добавлении фотографии нужно сохранить файл и
собрать метаинформацию
• При добавлении поста в ленту нужно распарсить и
обработать ссылки
• …
Нужен механизм для вызова произвольного кода!
35
Добавляем в определение метода коллбэки
prepare
• Вызывается до обращения к
нижележащему слою
• В качестве аргумента получает
входящие параметры метода
(для веб-слоя – объект формы)
• Может модифицировать
параметры (форму)
finish
• Вызывается после обращения к
нижележащему слою
• В качестве аргумента получает
данные, которые вернул
нижележащий слой
• Может модифицировать эти
данные
36
Для веб-слоя
our $valid_methods = {
method_name => {
prepare => sub {
my ($self, $form) = @_;
...
return $form;
},
finish => sub {
my ($self, $data) = @_;
...
return $data;
}
}
};
37
Чего мы добились
• Поигрались с кодогенерацией
• Убрали дублирование кода
• Формализовали декларацию данных для генерации
методов
• Научились добавлять вариативное поведение
Но нам все еще нужно добавлять по три файла,
в которых почти нет кода!
38
КОД, КОТОРОГО НЕ СУЩЕСТВУЕТ
39
Избавляемся от файлов-модулей
• Собираем воедино разрозненные определения методов
• Выделяем инструмент – генератор модулей
• Выносим отдельно заполнение определения методов
значениями по умолчанию
• Добавляем в определение HTTP-метод запроса (GET, POST,
PUT, DELETE)
• Строим роутинг веб-фреймворка
40
Новое определение метода в сервисе
• URL запроса (по умолчанию – имя_сервиса/имя_метода
• Метод запроса (по умолчанию – GET)
• Параметры веб-слоя
• Параметры сервисного слоя
• Параметры слоя данных
41
Параметры веб-слоя
• Проверка токена (по умолчанию включена)
• Проверка аутентификации (по умолчанию включена)
• Имя формы (по умолчанию имя_сервиса/имя_метода)
• Коллбэки prepare и finish (по умолчанию отсутствуют)
• Имя метода сервисного слоя (по умолчанию то же самое)
42
Параметры сервисного слоя
• Имя метода слоя данных (по умолчанию то же самое)
• Отправка уведомлений (по умолчанию выключена)
• Сохранение в историю (по умолчанию выключена)
• Формат возвращаемых данных (по умолчанию
определяется слоем данных)
• Коллбэки prepare и finish (по умолчанию отсутствуют)
43
Параметры слоя данных
• Взятие данных из кэша (по умолчанию выключено)
• Инвалидация кэша (по умолчанию выключена)
• Имя процедуры СУБД (по умолчанию
имя_сервиса.имя_метода)
• Набор входящих аргументов процедуры СУБД
• Формат возвращаемых данных (по умолчанию – таблица)
44
Простейшее определение метода
my $services => {
chat => {
message => {
data_layer => {
args => [qw(profile_id room_id reftime
message)],
},
},
},
};
45
Генератор сервиса
• 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
КОД, КОТОРЫЙ СУЩЕСТВУЕТ
47
Код, который существует
• Множество уже написанных модулей
• Нестардартные методы
48
Добавляем в генератор сервиса проверку 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
С 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
СЛЕДИТЕ ЗА РУКАМИ:
ПРОГРАММИРУЕМ НА КОНФИГАХ!
52
Декларациям место в текстовом формате
• Окончательно разделяем код и декларации
• Коллбэки prepare и finish выносим в модули, а в
декларациях оставляем имя модуля и функции
• Более компактный формат
• Легкое отключение сервиса на отдельных нодах: просто
удаляем конфиг!
53
Пример конфига
service: Chat
create_room:
web_layer:
form: chat/create_room
check_token: 1
check_auth: 1
method: put
url: common_chat/room/create
service_layer:
returns: room_id
finish: MyProject::Callback::Chat::gather_userinfo
notify: 1
data_layer:
func: chat.create_room
args: [profile_id, room_name, participant_id]
returns: single
54
ЧТО ДАЛЬШЕ?
55
ЕСТЬ ВОПРОСЫ?
56

Пластилиновый код: как перестать кодить и начать жить

  • 1.
    Пластилиновый код Как перестатькодить и начать жить Елена Шишкина, ведущий программист Деньги.Мэйл.Ру Москва, 2015 1
  • 2.
    ПОДУМАЕМ, КАК ЕГОНЕ ПИСАТЬ! Надоело писать код? 2
  • 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.
    Функция сервисного слоя submessage { 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.
    Функция слоя данных subcontactlist { 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.
    Можно как-нибудь так: subgod_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.
    Модули с фичами usestrict; 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
  • 18.
  • 19.
  • 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
  • 23.
  • 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
  • 27.
    Генератор метода длясервисного слоя 27
  • 28.
    use Sub::Name; use Storableqw(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
  • 31.
  • 32.
    use Sub::Name; use Storableqw(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
  • 34.
  • 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
  • 39.
    КОД, КОТОРОГО НЕСУЩЕСТВУЕТ 39
  • 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.
    Генератор сервиса • packageMyProject::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
  • 47.
  • 48.
    Код, который существует •Множество уже написанных модулей • Нестардартные методы 48
  • 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
  • 52.
  • 53.
    Декларациям место втекстовом формате • Окончательно разделяем код и декларации • Коллбэки prepare и finish выносим в модули, а в декларациях оставляем имя модуля и функции • Более компактный формат • Легкое отключение сервиса на отдельных нодах: просто удаляем конфиг! 53
  • 54.
    Пример конфига service: Chat create_room: web_layer: form:chat/create_room check_token: 1 check_auth: 1 method: put url: common_chat/room/create service_layer: returns: room_id finish: MyProject::Callback::Chat::gather_userinfo notify: 1 data_layer: func: chat.create_room args: [profile_id, room_name, participant_id] returns: single 54
  • 55.
  • 56.