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

56
Пластилиновый код Как перестать кодить и начать жить Елена Шишкина, ведущий программист Деньги.Мэйл.Ру Москва, 2015 1

Upload: moscowpm

Post on 21-Jul-2015

219 views

Category:

Technology


0 download

TRANSCRIPT

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

Пластилиновый код

Как перестать кодить и начать жить

Елена Шишкина, ведущий программист

Деньги.Мэйл.Ру

Москва, 2015

1

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

ПОДУМАЕМ, КАК ЕГО НЕ ПИСАТЬ!

Надоело писать код?

2

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

С чего все начиналось

• Веб-сервис (JSON API)

– nginx

– Mojolicious

– PostgreSQL

– Вся логика в процедурах СУБД

• Архитектура веб-приложения

– Вертикальная нарезка на сервисы: auth, profile, contactlist, chat, …

– Горизонтальная нарезка

• www-layer

• Service layer

• Data layer

– Сервисы могут обращаться к друг другу через service layer

3

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

Типичная функция веб-слоя

• Проверка CSRF-токена

• Аутентификация

• Авторизация

• Чтение и валидация входных данных

• Обращение к сервисному слою

• Перехват и маппинг ошибок

• Генерация вывода

4

Page 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

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

Типичная функция сервисного слоя

• Обработка входящих данных

• Обращение к слою данных

• Сохранение в ленту активности пользователя

• Рассылка уведомлений

• Подготовка возвращаемых данных

– result: OK или код ошибки

– собственно данные:

• нет данных

• одно значение

• хэш

• массив хэшей6

Page 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

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

Типичная функция слоя данных

• Запрос данных из кэша (для статических запросов)

• Вызов процедуры СУБД

• Сохранение в кэш

• Инвалидация кэша

• Нормализация выходных данных

8

Page 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

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

Новая фича

• 1 процедура СУБД

• 3 копипасты с небольшими изменениями

В половине случаев меняются только названия, ключи конфигов и имена процедур!

10

Page 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

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

Будем генерировать методы на лету

• Не делаем лишних телодвижений: генерируем из AUTOLOAD

• Чтобы не попросили странного, нам нужен список разрешенных методов

• Генератор в базовом классе, списки методов – в наследниках

• В наследниках можно описать вариации поведения

12

Page 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

Page 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

Page 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

Page 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

Page 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

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

Схема модулей

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

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

БОРЕМСЯ С ОДНОТИПНЫМ КОДОМ

19

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

Функция веб-слоя

Всегда:

• читает и валидирует входные параметры

• зовет сервисный слой

• перехватывает и маппит ошибки

• генерирует вывод

Может:

• читать определение веб-формы из разных конфигов

• проверять CSRF-токен

• проверять аутентификацию

• проверять авторизацию

20

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

Определение метода для веб-слоя

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

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

Определение метода для веб-слоя

use strict;use warnings;

package MyProject::Controller::Chat;use Mojo::Base 'MyProject::Controller::Base';

our $valid_methods = {message => { }

};

1;

22

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

Генератор для метода в веб-слое

23

Page 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

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

Функция сервисного слоя

Всегда:

• обращается к слою данных

• подготавливает возвращаемые данные

Может:

• обрабатывать входящие данные

• сохранять данные в ленту активности пользователя

• рассылать уведомления

• возвращать данные в разных структурах:

– нет данных (только код результата)

– одно значение

– хэш

– массив хэшей

25

Page 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

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

Генератор метода для сервисного слоя

27

Page 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

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

Функция слоя данных

Всегда:

• вызывает процедуру СУБД

Может:

• запрашивать данные из кэша

• передавать в процедуру разные наборы параметров

• читать результат работы процедуры в разном формате:

– нет возвращаемого значения

– одно значение

– строка

– таблица

• сохранять данные в кэш

• инвалидировать кэш

• нормализовывать выходные данные

29

Page 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

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

Генератор метода для слоя данных

31

Page 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

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

Чего мы добились

• Поигрались с кодогенерацией

• Убрали дублирование кода

• Формализовали декларацию данных для генерации методов

Получилась отличная модель, но…

33

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

БОРЕМСЯ С НЕОДНОТИПНЫМ КОДОМ

34

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

Гладко было на бумаге…

• После логина надо поставить куки

• После регистрации надо отправить email

• При добавлении фотографии нужно сохранить файл и собрать метаинформацию

• При добавлении поста в ленту нужно распарсить и обработать ссылки

• …

Нужен механизм для вызова произвольного кода!

35

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

Добавляем в определение метода коллбэки

prepare

• Вызывается до обращения к нижележащему слою

• В качестве аргумента получает входящие параметры метода (для веб-слоя – объект формы)

• Может модифицировать параметры (форму)

finish

• Вызывается после обращения к нижележащему слою

• В качестве аргумента получает данные, которые вернул нижележащий слой

• Может модифицировать эти данные

36

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

Для веб-слоя

our $valid_methods = {method_name => {

prepare => sub {my ($self, $form) = @_;...return $form;

},finish => sub {

my ($self, $data) = @_;...return $data;

}}

};

37

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

Чего мы добились

• Поигрались с кодогенерацией

• Убрали дублирование кода

• Формализовали декларацию данных для генерации методов

• Научились добавлять вариативное поведение

Но нам все еще нужно добавлять по три файла, в которых почти нет кода!

38

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

КОД, КОТОРОГО НЕ СУЩЕСТВУЕТ

39

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

Избавляемся от файлов-модулей

• Собираем воедино разрозненные определения методов

• Выделяем инструмент – генератор модулей

• Выносим отдельно заполнение определения методов значениями по умолчанию

• Добавляем в определение HTTP-метод запроса (GET, POST, PUT, DELETE)

• Строим роутинг веб-фреймворка

40

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

Новое определение метода в сервисе

• URL запроса (по умолчанию – имя_сервиса/имя_метода

• Метод запроса (по умолчанию – GET)

• Параметры веб-слоя

• Параметры сервисного слоя

• Параметры слоя данных

41

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

Параметры веб-слоя

• Проверка токена (по умолчанию включена)

• Проверка аутентификации (по умолчанию включена)

• Имя формы (по умолчанию имя_сервиса/имя_метода)

• Коллбэки prepare и finish (по умолчанию отсутствуют)

• Имя метода сервисного слоя (по умолчанию то же самое)

42

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

Параметры сервисного слоя

• Имя метода слоя данных (по умолчанию то же самое)

• Отправка уведомлений (по умолчанию выключена)

• Сохранение в историю (по умолчанию выключена)

• Формат возвращаемых данных (по умолчанию определяется слоем данных)

• Коллбэки prepare и finish (по умолчанию отсутствуют)

43

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

Параметры слоя данных

• Взятие данных из кэша (по умолчанию выключено)

• Инвалидация кэша (по умолчанию выключена)

• Имя процедуры СУБД (по умолчанию имя_сервиса.имя_метода)

• Набор входящих аргументов процедуры СУБД

• Формат возвращаемых данных (по умолчанию – таблица)

44

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

Простейшее определение метода

my $services => {

chat => {

message => {

data_layer => {

args => [qw(profile_id room_id reftime

message)],

},

},

},

};

45

Page 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

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

КОД, КОТОРЫЙ СУЩЕСТВУЕТ

47

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

Код, который существует

• Множество уже написанных модулей

• Нестардартные методы

48

Page 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

Page 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

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

Чего мы добились

• Поигрались с кодогенерацией

• Убрали дублирование кода

• Формализовали декларацию данных для генерации методов

• Научились добавлять вариативное поведение

• Собрали определение сервисов и методов воедино

• Сделали определение типичных методов максимально лаконичным

• Для новых и отлично ложащихся в шаблон сервисов мы даже избавились от модулей

• Но при этом сохранили обратную совместимость

• А также возможность добавлять нестандартные методы

• Ленивая инициализация – при запуске сервера не генерируется ничего лишнего

Но определение сервиса и метода –это все еще код! 51

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

СЛЕДИТЕ ЗА РУКАМИ: ПРОГРАММИРУЕМ НА КОНФИГАХ!

52

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

Декларациям место в текстовом формате

• Окончательно разделяем код и декларации

• Коллбэки prepare и finish выносим в модули, а в декларациях оставляем имя модуля и функции

• Более компактный формат

• Легкое отключение сервиса на отдельных нодах: просто удаляем конфиг!

53

Page 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

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

ЧТО ДАЛЬШЕ?

55

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

ЕСТЬ ВОПРОСЫ?

56