Со своей стороны я буду рад получить критику и замечания по поводу кода.
Итак представим себе что наша цель — универсальный интерфейс для хранения пар ключ-значение, не вдаваясь в подробности имплементации. Все что мы хотим сделать на первом этапе — определить какой интерфейс будет у «рабочей» части:
-callback list_items() -> [term()].
-callback add_item(Id :: term(), Value :: any() )->ok|{error, any() }.
-callback get_item(Id :: term()) -> {ok, Value::any()}|{error, not_found}.
-callback del_item(Id :: term()) -> ok|{error, not_found}.
Интерфейс очень прост и никак не определяет где хранятся значения — в дереве, хэш-таблице или простом списке.
Следующий момент — какую бы имплементацию мы бы не собрались определять, у нее обязательно будет внутреннее состояние — то самое дерево, таблица или список. И что-бы хранить его мы воспользуемся стандартным gen_server-ом. То есть у нас будет код реализующий интерфейс, опирающийся на состояние предоставленное модулем имплементируюшим gen_server. Вот она и цепочка.
Теперь момент инициализации. При старте модуля имплементации, мы должны вместо себя стартовать модуль интерфейса, а он в свою очередь обратится к gen_server-у.
Где-то так:
Имплементация:
start_link(_Params)->
storage:start_link(example_storage, []).
Интерфейс:
start_link(Module, ModArgs) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, {Module, ModArgs}, []).
Следующий шаг в цепочке вызова — функция init в модуле интерфейса. Ей в свою очередь неплохо бы вызвать соответствующую функцию в модуле имплементации. К тому-же нам нужно сохранить всю эту информацию. Итого:
Интерфейс:
-record(state, {mod :: atom(), mod_state :: term() }).
init({Mod, ModArgs}) ->
process_flag(trap_exit, true),
{ok, ModState} = Mod:init_storage(ModArgs),
{ok, #state{mod = Mod, mod_state = ModState}}.
Имплементация:
init_storage([])->
Table = [],
{ok, [{table, Table }]}.
Да, да! Эта имплементация хранит данные в простом списке. Ужас.
Это все об инициализации. Теперь вернемся к рабочей части. Наши данные хранятся внутри статуса интерфейса. Так давайте вызовем его и попросим обработать:
Имплементация:
-spec list_items() -> [term()].
list_items()->
storage:do_action(list_items, {}).
Интерфейс:
-spec do_action(atom(), tuple())-> term().
do_action(Command, Command_params)->
gen_server:call(?MODULE, {Command, Command_params}).
handle_call({Command, Command_params}, _From, State = #state{mod = Mod, mod_state = ModState}) ->
{Reply, UpdatedState} = Mod:execute_action(ModState, Command, Command_params),
{reply, Reply, State#state{mod_state = UpdatedState}}.
Мы получаем вызов, получаем внутреннее состояние и вызываем функцию которая и будет обрабатывать запрос.
Имплементация:
-spec execute_action(State::term(), Action::atom(), ActionParams :: tuple() ) -> {term(), term()}.
execute_action(State = [{table, Table }], list_items, {}) ->
{Table, State}.
А теперь соберем все вместе:
Интерфейс:
-module(storage).
-behaviour(gen_server).
-callback start_link(Params :: [term()])-> {ok, pid() }.
-callback init_storage(Args :: list())->
{ok, State :: term()}.
-callback list_items() -> [term()].
-callback add_item(Id :: term(), Value :: any() )->ok|{error, any() }.
-callback get_item(Id :: term()) -> {ok, Value::any()}|{error, not_found}.
-callback del_item(Id :: term()) -> ok|{error, not_found}.
-callback execute_action(State::term(), Action::atom(), ActionParams :: tuple() ) -> {term(), term()}.
-export([start_link/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([do_action/2]).
-define(SERVER, ?MODULE).
-record(state, {mod :: atom(), mod_state :: term() }).
start_link(Module, ModArgs) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, {Module, ModArgs}, []).
-spec do_action(atom(), tuple())-> term().
do_action(Command, Command_params)->
gen_server:call(?MODULE, {Command, Command_params}).
init({Mod, ModArgs}) ->
process_flag(trap_exit, true),
{ok, ModState} = Mod:init_storage(ModArgs),
{ok, #state{mod = Mod, mod_state = ModState}}.
handle_call({Command, Command_params}, _From, State = #state{mod = Mod, mod_state = ModState}) ->
{Reply, UpdatedState} = Mod:execute_action(ModState, Command, Command_params),
{reply, Reply, State#state{mod_state = UpdatedState}}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
И имплементация:
-module(example_storage).
-behaviour(storage).
-export([start_link/1, init_storage/1, list_items/0, add_item/2, get_item/1, del_item/1, execute_action/3]).
-spec start_link(Params :: list())-> {ok, pid() }.
start_link(_Params)->
storage:start_link(example_storage, []).
-spec init_storage(Args :: list())->
{ok, State :: term()}.
init_storage([])->
Table = [],
{ok, [{table, Table }]}.
-spec list_items() -> [term()].
list_items()->
storage:do_action(list_items, {}).
-spec add_item(Id :: term(), Value :: any() )->ok|{error, any() }.
add_item(Id, Value)->
storage:do_action(add_item, {Id, Value}).
-spec get_item(Id :: term()) -> {ok, Value::any()}|{error, not_found}.
get_item(Id)->
storage:do_action(get_item, {Id}).
-spec del_item(Id :: term()) -> ok|{error, not_found}.
del_item(Id)->
storage:do_action(del_item, {Id}).
-spec execute_action(State::term(), Action::atom(), ActionParams :: tuple() ) -> {term(), term()}.
execute_action(State = [{table, Table }], list_items, {}) ->
{Table, State};
execute_action(_State = [{table, Table }], add_item, {Id, Value}) ->
UpdatedTable = lists:keystore(Id, 1, Table, {Id, Value}),
{ok, [{table, UpdatedTable }]};
execute_action(State = [{table, Table }], get_item, {Id}) ->
case lists:keyfind(Id, 1, Table) of
false ->
{ {error, not_found}, State};
{Id, Value} ->
{Value, State}
end;
execute_action(State = [{table, Table }], del_item, {Id}) ->
case lists:keymember(Id, 1, Table) of
true ->
UpdatedTable = lists:keydelete(Id, 1, Table),
{ok, [{table, UpdatedTable }] };
false ->
{ {error, not_found}, State}
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
simple_test()->
{ok, Pid} = example_storage:start_link([]),
[] = example_storage:list_items(),
ok = example_storage:add_item(key1, 2),
[{key1,2}] = example_storage:list_items(),
ok = example_storage:add_item(key2, 4),
[{key1,2}, {key2, 4}] = example_storage:list_items(),
ok = example_storage:del_item(key1),
[{key2, 4}] = example_storage:list_items(),
{error, not_found} = example_storage:del_item(key1),
{error, not_found} = example_storage:get_item(key1),
4 = example_storage:get_item(key2),
ok = example_storage:del_item(key2),
[] = example_storage:list_items().
-endif.
Это простая и очень наивная реализация. В реальной жизни модуль интерфейса содержит общий для разных решений код и вспомогательные функции. Обобщеный подход делает код более гибким и упрощает тестирование.
Комментарии (4)
begemot_sun
23.01.2018 20:13А почему цепочка поведения? вроде как это просто «поведение»?
А цель введения ген_сервера? Обычно имплементация поведения идет в разрезе:
Mod:some_action
где Mod — переменная в которой хранится атом — имя модуля.
В вашем случае ген_сервер просто реализует некоторый функционал, но никто и нигде не знает «тип» этого ген_сервера.
absurdil Автор
24.01.2018 11:41Это пример того как оно работает ( или может работать) в случае когда такая цепочка оправдана.
А это — пример.
В реальности вы же используете supervisor. Потому-что он дает явное преимущество и реализует нужный функционал. А то что он в свою очередь построен на gen_server не является информацией необходимой для его использования.
В реальности у меня были очень оправданые случаи, например при реализации storage api для GCS и S3. XML API для GCS и API S3 очень похожи. Разница в основном в основных путях, создании подписей и других подобных мелочах. Но основной код был одинаков и был вынесен в отдельное поведение. Которое в свою очередь наследовало gen_server для ключей, контроля и прочих мелочей.
maxzhurkin
Что за окончании в заголовке?
absurdil Автор
Спасибо