23 January 2018

Cоздание цепочки поведений

Erlang/OTP
Всем привет! В первой заметке я довольно поверхностно упомянул о создании цепочки поведений. В этой я хочу дать пример простой цепочки с пояснениями.

Со своей стороны я буду рад получить критику и замечания по поводу кода.

Итак представим себе что наша цель — универсальный интерфейс для хранения пар ключ-значение, не вдаваясь в подробности имплементации. Все что мы хотим сделать на первом этапе — определить какой интерфейс будет у «рабочей» части:

-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.

Это простая и очень наивная реализация. В реальной жизни модуль интерфейса содержит общий для разных решений код и вспомогательные функции. Обобщеный подход делает код более гибким и упрощает тестирование.
Tags:erlang/otp
Hubs: Erlang/OTP
+8
2.5k 12
Comments 4
Popular right now