2012-10-07 6 views
6

Betrachten Sie, ich habe eine FSM mit gen_fsm implementiert. Für ein Event in einem StateName sollte ich Daten in die Datenbank schreiben und dem User das Ergebnis antworten. So ist der folgende Statusname durch eine Funktion dargestellt wird:Grundlagen von OTP. Wie trennt man funktionalen und nicht-funktionalen Code in der Praxis?

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. 

wo my_db_module: Schreib ist ein Teil des nicht-funktionalen Code tatsächliche Datenbank-Write-Implementierung.

Ich sehe zwei große Probleme mit diesem Code: der erste, ein reines funktionales Konzept von FSM ist durch einen Teil des nicht-funktionalen Codes gemischt, dies macht auch Unit-Tests von FSM unmöglich. Zweitens hängt ein Modul, das eine FSM implementiert, von einer bestimmten Implementierung von my_db_module ab.

Meiner Meinung nach, sind zwei Lösungen möglich:

  1. Implement my_db_module: write_async als eine asynchrone Nachricht zu einem gewissen Prozessabwicklung Datenbank zu senden, antworten Sie nicht in state, speichern Von in StateData, wechseln Sie in wait_for_db_answer und Warten Sie auf das Ergebnis des Datenbankverwaltungsprozesses als Nachricht in einer handle_info.

    Vorteile einer solchen Implementierung ist die Möglichkeit, beliebige Nachrichten von eunit-Modulen zu senden, ohne die eigentliche Datenbank zu berühren. Die Lösung leidet an möglichen Wettlaufbedingungen, wenn db früher antwortet, dass FSM den Status ändert oder ein anderer Prozess save_data an FSM sendet.

  2. eine Rückruffunktion verwenden, geschrieben während 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. 
    

    Diese Lösung leidet nicht unter Rennbedingungen, aber wenn FSM es viele Rückrufe verwendet überwältigt wirklich den Code. Obwohl der Wechsel zum tatsächlichen Funktionsrückruf das Komponententest ermöglicht, löst er das Problem der funktionalen Codetrennung nicht.

Ich bin nicht mit all diesen Lösungen zufrieden. Gibt es ein Rezept, um dieses Problem auf eine reine OTP/Erlang Weise zu behandeln? Vielleicht ist es mein Problem, Prinzipien von OTP und Eunit zu untermauern.

Antwort

2

Eine Möglichkeit, dies zu lösen, ist über Dependency Injection des Datenbankmoduls.

Sie definieren Ihr Zustand Rekord als

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

Und jetzt können Sie db_mod auf init/1 des gen_server injizieren:

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

Also, wenn wir den Code haben:

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. 

Wir haben die Möglichkeit, das Datenbankmodul beim Testen mit einem anderen Modul zu überschreiben. Das Einfügen eines Stubs ist jetzt ziemlich einfach und Sie können somit die Darstellung des Datenbankcodes nach Belieben ändern.

Eine andere Alternative ist, ein Tool wie meck zu verwenden, um das Datenbankmodul beim Testen zu testen, aber ich bevorzuge es normalerweise, es konfigurierbar zu machen.obwohl

Im Allgemeinen Ich neige dazu, den Code zu spalten, die komplex in sein eigenes Modul ist so können sie separat getestet werden. Ich teste selten viele Unit-Tests anderer Module und bevorzuge umfangreiche Integrationstests, um Fehler in solchen Teilen zu behandeln. Werfen Sie einen Blick auf Common Test, PropEr, Triq und Erlang QuickCheck (Letzteres ist keine Open Source, noch ist die Vollversion frei).