83

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

 5 years ago
source link: http://szpzs.oschina.io/2018/06/15/railway-oriented-development-with-erlang/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

准备开始

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

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

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

FNvAZfA.png!web

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

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).

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

增加错误处理

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

nyIzYry.png!web

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

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

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 %……可读性也相应降低了!

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

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

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

fQj2emm.jpg!web

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

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

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

RnuuEzI.jpg!web

所以,简单来说,确切的情况是:

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

jyQzEfv.jpg!web

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

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

-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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK