用铁路的方法简化Erlang的case嵌套问题

本文提出了另一种可以构建你的程序的方法,它的灵感来自Elixir的管道宏’|>’,此方法就是使用我最近编写的小型epipe库,而不用令人可怕的parse transforms。 Epipe本身的灵感来源于Scott Wlaschin发表的这篇文章

准备开始

我们来执行一个小实践任务,它将演示这种铁路方式的函数式编程方法。

考虑一下我们使用Erlang构建POP3电子邮件客户端的情况。 我们的目标是实现与POP服务器建立连接的控制流程。

下图说明完成此操作所需的步骤:

首先,让我们写一个函数来实现建立连接的功能:

1
2
3
4
5
connect(Addr, Port, ConnOptions, User, Password) ->
{ok, Socket} = ssl:connect(Addr, Port, ConnOptions),
ok = receive_greetings(Socket),
ok = send_user(Socket, User),
ok = send_password(Socket, Password).

上述代码非常漂亮,仅仅四行就完成了我们想要的功能。但是等等……上面的实现是非常完美的场景。 显然我们需要添加一些错误处理来应对边界条件的场景 :( 。我的意思是,“可能会出错的地方”?

增加错误处理

让我们总结如下图中所有可能的边界情况:

我们增加错误处理的代码,然后看看代码变成什么样子!

剧透:下面的例子很简单,可以通过将操作分成单独的函数来美化,但是嵌套的case语句是不可避免的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
connect(Addr, Port, ConnOptions, User, Password) ->
case ssl:connect(Addr, Port, ConnOptions) of
{ok, Socket} ->
case receive_greetings(Socket) of
ok ->
case send_user(Socket, User) of
ok ->
case send_password(Socket, Password) of
ok -> ok;
_Err -> error_logger:error_msg("Auth error")
end;
_Err ->
error_logger:error_msg("Unknown user")
end;
Err -> error_logger:error_msg("Could not receive_greetings")
end;
_Error -> error_logger:error_msg("Could not connect")
end.

现在我们添加了所有的错误处理代码。但是,代码的大小增加了400 %……可读性也相应降低了!

也许有一个更清晰的方式来实现这一点?

用“铁路”的方法来设计更好的错误处理(理论)

铁路方法背后的想法是用铁路道岔作为模拟物来分解“每一步”功能块:

这种方法可以翻译为如下的Erlang代码:

1
2
3
4
5
switch_component(Input) ->
case some_action() of
{ok, Response} -> {ok, Response}; % Green track
Error -> {error, Error} % Red track
end.

一旦为所有需要的操作创建了两种方式( ok / error )切换分支,就可以像在铁路上一样优雅地组合它们:

所以,简单来说,确切的情况是:
在成功的情况下,所有功能(“铁路道岔”)都按顺序执行,我们沿着“成功轨道”行进。否则,我们的列车将切换到“错误轨道”,并沿该路线行驶,绕过所有其他步骤:

用“铁路”的方法来设计更好的错误处理

我们发布了一个很小的Erlang库,它简化了Erlang的“铁路道岔”分解方式。那么,考虑上面的例子,让我们来看一下如何使用Epipe实现我们的用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
-record(connection, {
socket,
user,
addr,
port,
passwd
}).
connect(Addr, Port, User, Password) ->
Connection = #connection{
user = User,
passwd = Password,
add = Addr,
port = Port
},
% 定义要遵循的铁路道岔列表
ConnectionSteps = [
{get_socket, fun get_socket/1},
{recv_greetings, fun recv_greetings/1},
{send_user, fun send_user/1},
{send_passwd, fun send_passwd/1}
],
% 通过道岔运行
case epipe:run(ConnectionSteps, Connection) of
{error, Step, Reason, _State} ->
error_logger:error_msg("Failed to establish connection. Reason: ~p", [Step]),
{error, Reason};
{ok, _Conn} = Success -> Success
end.
% 构建功能块。注意:每一个函数可以返回 {ok, Connection} 或 {error, Reason}
get_socket(Connection) ->
case ssl:connect(Addr, Port, ExtraOptions) of
{ok, Socket} -> {ok, Connection#connection{socket = Socket}};
Error -> {error, Error}
end.
recv_greetings(Connection) ->
case recv(Connection) of
{ok, <<"+OK", _Rest/binary>>} -> {ok, Connection};
{ok, <<"-ERR ", Error/binary>>} -> {error, Error};
Err -> {error, Err}
end.
send_user(Connection = #connection{user = User}) ->
Msg = list_to_binary(User),
send(Connection, <<"USER ", Msg/binary>>),
case recv(Connection) of
{ok, <<"+OK", _Rest/binary>>} -> {ok, Connection};
{ok, <<"-ERR ", Error/binary>>} -> {error, Error};
Err -> {error, Err}
end.
send_passwd(Connection = #connection{passwd = Passwd}) ->
Msg = list_to_binary(Passwd),
send(Connection, <<"PASS ", Msg/binary>>),
case recv(Connection) of
{ok, <<"+OK", _Rest/binary>>} -> {ok, Connection};
{ok, <<"-ERR ", Error/binary>>} -> {error, Error};
Err -> {error, Err}
end.

总结

与嵌套的case语句实现相比,最终的代码在代码行方面不会更小,但它确实更具可读性,使得调试和支持变得更加容易。

如果你希望看到真实的实现案例,请查看使用铁路方法执行的重构例子

原文链接: https://www.erlang-solutions.com/blog/railway-oriented-development-with-erlang.html