Пластилиновый код
Как перестать кодить и начать жить
Елена Шишкина, ведущий программист
Деньги.Мэйл.Ру
Москва, 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_methodAUTOLOAD
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;
packageMyProject::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