OTP的原则。如何在实践中分离功能和非功能代码?

时间:2012-10-07 13:31:03

标签: erlang otp

考虑我使用gen_fsm实现了FSM。对于某个StateName中的某个事件,我应该将数据写入数据库并向用户回复结果。因此,以下StateName由函数表示:

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.

其中my_db_module:write是实现实际数据库写入的非功能代码的一部分。

我看到这个代码存在两个主要问题:第一,FSM的纯功能概念与非功能代码的一部分混合在一起,这也使得FSM的单元测试变得不可能。其次,实现FSM的模块依赖于my_db_module的特定实现。

在我看来,有两种解决方案是可能的:

  1. 实现my_db_module:write_async作为向某个进程处理数据库发送异步消息,不回复statename,在StateData中保存From,切换到wait_for_db_answer并将数据库管理进程中的结果作为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.
    

    这种实现的优点是可以从eunit模块发送任意消息而无需触及实际数据库。解决方案遇到可能的竞争条件,如果db先前回复,FSM更改状态或另一个进程将save_data发送到FSM。

  2. 使用在StateData中的init / 1期间编写的回调函数:

    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.
    

    这个解决方案不会受到竞争条件的影响,但如果FSM使用了很多回调,那么它真的会压倒代码。虽然改为实际函数回调可以使单元测试成为可能,但它并没有解决功能代码分离的问题。

  3. 我对所有这些解决方案都不满意。是否有一些配方以纯OTP / Erlang方式处理此问题?可能是我的问题是低估了OTP和eunit的原则。

1 个答案:

答案 0 :(得分:2)

解决此问题的一种方法是通过数据库模块的依赖注入。

您将州记录定义为

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

现在你可以在gen_server的init / 1上注入db_mod:

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

所以当我们有你的代码时:

 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.

我们可以在使用其他模块进行测试时覆盖数据库模块。现在可以很容易地注入存根,因此您可以根据需要更改数据库代码表示。

另一种方法是在测试时使用像meck这样的工具来模拟数据库模块,但我通常更喜欢使其可配置。

总的来说,我倾向于将复杂的代码拆分为自己的模块,因此可以单独测试。我很少对其他模块进行大量单元测试,而更喜欢大规模集成测试来处理这些部件中的错误。看看Common Test,PropEr,Triq和Erlang QuickCheck(后者不是开源的,也不是免费的完整版)。