📜 ⬆️ ⬇️

Creating a chain of behaviors

Hello! In the first note, I rather superficially mentioned the creation of a chain of behaviors. In this, I want to give an example of a simple chain with explanations.

For my part, I will be happy to receive criticism and comments about the code.

So let's imagine that our goal is a universal interface for storing key-value pairs, without going into implementation details. All we want to do in the first stage is to determine which interface the “working” part will have:

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

The interface is very simple and in no way determines where values ​​are stored - in a tree, a hash table, or a simple list.
')
The next moment is no matter what implementation we are going to determine, it will necessarily have an internal state - the same tree, table or list. And to store it, we will use the standard gen_server. That is, we will have a code that implements the interface, based on the state provided by the module to implement the gen_server. Here it is a chain.

Now the moment of initialization. When the implementation module starts, we must start the interface module instead, and it in turn will turn to gen_server.

Somewhere like this:

Implementation:

 start_link(_Params)-> storage:start_link(example_storage, []). 

Interface:

 start_link(Module, ModArgs) -> gen_server:start_link({local, ?SERVER}, ?MODULE, {Module, ModArgs}, []). 

The next step in the call chain is the init function in the interface module. She, in turn, would like to call the corresponding function in the implementation module. Besides, we need to save all this information. Total:

Interface:

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

Implementation:

 init_storage([])-> Table = [], {ok, [{table, Table }]}. 

Yes Yes! This implementation stores data in a simple list. Horror.

It's all about initialization. Now back to the working part. Our data is stored within the status of the interface. So let's call it and ask to process:

Implementation:

 -spec list_items() -> [term()]. list_items()-> storage:do_action(list_items, {}). 

Interface:

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

We receive a call, receive an internal state and call the function that will process the request.

Implementation:

 -spec execute_action(State::term(), Action::atom(), ActionParams :: tuple() ) -> {term(), term()}. execute_action(State = [{table, Table }], list_items, {}) -> {Table, State}. 

Now let's put it all together:

Interface:

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

And implementation:

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

This is a simple and very naive implementation. In real life, the interface module contains common for different solutions code and auxiliary functions. The generalized approach makes the code more flexible and simplifies testing.

Source: https://habr.com/ru/post/347370/


All Articles