考虑我有一个用 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 的特定实现。
在我看来,两种解决方案是可能的:
实现 my_db_module:write_async 作为向某个进程处理数据库发送异步消息,不要在 statename 中回复,将 From 保存在 StateData 中,切换到 wait_for_db_answer 并等待来自 db 管理进程的结果作为句柄信息中的消息。
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,则该解决方案可能会遇到竞争条件。
使用在 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 使用许多回调,它确实会使代码不堪重负。尽管更改为实际的函数回调使单元测试成为可能,但它并没有解决函数代码分离的问题。
我对所有这些解决方案都不满意。是否有一些方法可以以纯 OTP/Erlang 方式处理这个问题?可能是我理解 OTP 和 eunit 的原理的问题。