2012-10-07 43 views
6

考慮我有一個的gen_fsm實現的FSM。對於某個StateName中的某個事件,我應該將數據寫入數據庫並回複用戶結果。所以下面Statename的由函數表示:OTP原理。如何在實踐中分離功能和非功能的代碼?

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:寫入是在實施實際的數據庫寫入非功能碼的一部分。

我看到兩個主要問題與此代碼:第一,FSM的純功能概念是由非功能性的代碼的一部分混合,這也使得FSM的單元測試是不可能的。其次,實現FSM的模塊依賴於my_db_module的特定實現。

在我看來,兩種解決方案:

  1. 實施my_db_module:write_async作爲發送異步消息在一定工藝處理數據庫,在Statename的不回覆,保存在StateData,切換到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模塊發送任意消息,而不需要觸及真正的數據庫。該解決方案從可能的競爭條件下受苦,如果分貝的答覆早些時候,該FSM改變狀態或其他進程發送save_data到FSM。

  2. 使用一個回調函數,在編寫初始化/ 1 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. 
    

    此解決方案不從比賽條件受到影響,但如果FSM使用很多回調果然壓倒了代碼。儘管更改爲實際函數回調使得單元測試成爲可能,但它不能解決功能代碼分離的問題。

我對所有這些解決方案並不滿意。是否有一些處理純OTP/Erlang方式的問題?可能是我低估了OTP和eunit原理的問題。要解決這個

回答

2

一種方式是通過數據庫模塊的依賴注入。

定義你的狀態記錄爲

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

現在你可以於gen_server的初始化/ 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(後者不是開源的,也不是免費的完整版本)。