2012-10-07 7 views
6

Considera che ho implementato un FSM con gen_fsm. Per alcuni eventi in un qualche StateName dovrei scrivere i dati nel database e rispondere all'utente al risultato. Quindi il seguente StateName è rappresentato da una funzione:Principi di OTP. Come separare il codice funzionale e non funzionale nella pratica?

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. 

dove my_db_module: scrittura è una parte di codice non funzionale applicazione effettiva scrittura del database.

Vedo due problemi principali con questo codice: il primo, un concetto puramente funzionale di FSM è misto a parte di codice non funzionale, questo rende impossibile anche il test dell'unità di FSM. Secondo, un modulo che implementa un FSM ha dipendenza da un'implementazione particolare di my_db_module.

A mio parere, due soluzioni sono possibili:

  1. Implementare my_db_module: write_async come l'invio di un messaggio asincrono ad alcuni database di gestione dei processi, non si risponde in StateName, salvare da in StateData, passare a wait_for_db_answer e attendere il risultato del processo di gestione db come messaggio 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. 
    

    vantaggi di tale implementazione è possibilità di inviare messaggi arbitrarie da moduli eunit senza toccare database effettivo. La soluzione soffre di possibili condizioni di gara, se db risponde prima, che FSM cambia stato o un altro processo invia save_data a FSM.

  2. Utilizzare una funzione di callback, scritto durante 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. 
    

    Questa soluzione non soffre di condizioni di gara, ma se FSM utilizza molti callback davvero travolge il codice. Sebbene il passaggio alla funzione effettiva di callback renda possibile il test delle unità, non risolve il problema della separazione dei codici funzionali.

Non mi sento a mio agio con tutte queste soluzioni. C'è qualche ricetta per gestire questo problema in modo puro OTP/Erlang? Di certo è il mio problema di sottovalutare i principi di OTP e eunit.

risposta

2

Un modo per risolvere questo è tramite l'iniezione delle dipendenze del modulo del database.

definite il vostro record di Stato come

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

e ora si può iniettare db_mod su init/1 del gen_server:

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

Così, quando abbiamo il codice:

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. 

abbiamo la possibilità di sovrascrivere il modulo del database durante il test con un altro modulo. L'iniezione di uno stub è ora piuttosto semplice e puoi quindi modificare la rappresentazione del codice del database come meglio credi.

Un'altra alternativa è utilizzare uno strumento come meck per simulare il modulo del database durante il test, ma di solito preferisco renderlo configurabile.

In generale, tuttavia, tendo a dividere il codice che è complesso nel proprio modulo in modo che possa essere testato separatamente. Raramente faccio molti test di unità di altri moduli e preferisco test di integrazione su larga scala per gestire gli errori in tali parti. Dai un'occhiata a Common Test, PropEr, Triq e Erlang QuickCheck (quest'ultimo non è open source, né la versione completa è gratuita).