Principles of OTP. How to separate functional and non-functional code in practice?

I have FSM implemented with gen_fsm. For some event in some StateName, I have to write data to the database and respond to the user result. Thus, the following StateName is represented by a function:

statename(Event, _From, StateData) when Event=save_data-> case my_db_module:write(StateData#state.data) of ok -> {stop, normal, ok, StateData}; _ -> {reply, database_error, statename, StateData) end. 

where my_db_module: write is part of non-functional code that implements the actual database record.

I see two main problems with this code: the first, purely functional FSM concept is mixed with part of non-functional code, this also makes unit testing FSM impossible. Secondly, a module that implements FSM has a dependency on the specific implementation of my_db_module.

In my opinion, two solutions are possible:

  • Implement my_db_module: write_async as sending an asynchronous message to some process processing database, do not respond to statename, save From in StateData, switch to wait_for_db_answer and wait for the result from the db control process as a message in handle_info.

     statename(Event, From, StateData) when Event=save_data-> my_db_module:write_async(StateData#state.data), NewStateData=StateData#state{from=From}, {next_state,wait_for_db_answer,NewStateData} handle_info({db, Result}, wait_for_db_answer, StateData) -> case Result of ok -> gen_fsm:reply(State#state.from, ok), {stop, normal, ok, State}; _ -> gen_fsm:reply(State#state.from, database_error), {reply, database_error, statename, StateData) end. 

    The advantages of this implementation are the ability to send arbitrary messages from eunit modules without touching the actual database. The decision may be related to the possible conditions of the race if the answer is db earlier that the FSM changes state or another process sends save_data to the FSM.

  • Use the callback function recorded during init / 1 in StateData:

     init([Callback]) -> {ok, statename, #state{callback=Callback}}. statename(Event, _From, StateData) when Event=save_data-> case StateData#state.callback(StateData#state.data) of ok -> {stop, normal, ok, StateData}; _ -> {reply, database_error, statename, StateData) end. 

    This solution does not suffer from race conditions, but if the FSM uses a lot of callbacks, it really overloads the code. Although switching to the actual function callback makes unit testing possible, it does not solve the problem of functional code separation.

I do not like all these decisions. Is there a recipe to solve this problem in pure OTP / Erlang? Perhaps this is my problem of understating the principles of OTP and eunit.

+6
source share
1 answer

One way to solve this problem is to embed the Dependency Injection module into the database.

You define your status record as

  -record(state, { ..., db_mod }). 

And now you can add db_mod to init / 1 gen_server:

  init([]) -> {ok, DBMod} = application:get_env(my_app, db_mod), ... {ok, #state { ..., db_mod = DBMod }}. 

So, when we have the code:

  statename(save_data, _From, #state { db_mod = DBMod, data = Data } = StateData) -> case DBMod:write(Data) of ok -> {stop, normal, ok, StateData}; _ -> {reply, database_error, statename, StateData) end. 

We have the opportunity to override the database module when testing with another module. Implementing a stub is now quite easy, and you can thus change the way the database code is presented as you see fit.

Another alternative is to use a tool like meck to mock the database module during testing, but I usually prefer to tune it.

In general, I usually break code that is complex into my own module, so it can be tested separately. I rarely do many unit tests of other modules and prefer large-scale integration tests to handle errors in such detail. Take a look at Common Test, PropEr, Triq and Erlang QuickCheck (the latter is not open source and is not a full version).

+2
source

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


All Articles