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

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

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

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

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

    Это простая и очень наивная реализация. В реальной жизни модуль интерфейса содержит общий для разных решений код и вспомогательные функции. Обобщеный подход делает код более гибким и упрощает тестирование.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      0
      Что за окончании в заголовке?
        0
        Спасибо
        0
        А почему цепочка поведения? вроде как это просто «поведение»?
        А цель введения ген_сервера? Обычно имплементация поведения идет в разрезе:
        Mod:some_action

        где Mod — переменная в которой хранится атом — имя модуля.
        В вашем случае ген_сервер просто реализует некоторый функционал, но никто и нигде не знает «тип» этого ген_сервера.

          0
          Это пример того как оно работает ( или может работать) в случае когда такая цепочка оправдана.
          А это — пример.
          В реальности вы же используете supervisor. Потому-что он дает явное преимущество и реализует нужный функционал. А то что он в свою очередь построен на gen_server не является информацией необходимой для его использования.

          В реальности у меня были очень оправданые случаи, например при реализации storage api для GCS и S3. XML API для GCS и API S3 очень похожи. Разница в основном в основных путях, создании подписей и других подобных мелочах. Но основной код был одинаков и был вынесен в отдельное поведение. Которое в свою очередь наследовало gen_server для ключей, контроля и прочих мелочей.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое