一、为什么Erlang代码需要重构

Erlang是一门强大的函数式编程语言,特别适合构建高并发、分布式系统。但随着时间的推移,很多遗留系统会积累大量"技术债"——代码结构混乱、模块耦合度高、文档缺失等问题逐渐浮现。想象一下,你接手了一个运行了10年的电信计费系统,打开代码一看:

  • 某个模块超过5000行代码
  • 函数参数多达15个
  • 模式匹配嵌套了8层
  • 到处是catch _:_ -> ok这样的万能异常处理

这时候,重构就变得非常必要了。重构不是重写,而是在保持外部行为不变的前提下,改善代码内部结构。就像给老房子做装修,不动承重墙,但要让管线更合理、空间更宜居。

二、Erlang代码的常见"坏味道"

让我们看几个典型的Erlang代码问题,以及如何识别它们:

1. 超长函数

%% 问题示例:处理用户订单的超级函数
process_order(Order) ->
    % 验证步骤(200行)
    case validate_order(Order) of
        {error, Reason} -> {error, Reason};
        {ok, ValidOrder} ->
            % 计算步骤(300行)
            case calculate_price(ValidOrder) of
                {error, Reason} -> {error, Reason};
                {ok, Price} ->
                    % 持久化步骤(400行)
                    case save_to_database(ValidOrder, Price) of
                        {error, Reason} -> {error, Reason};
                        {ok, _} ->
                            % 通知步骤(100行)
                            notify_user(ValidOrder)
                    end
            end
    end.

这种"面条式"代码难以维护,任何修改都可能引发连锁反应。

2. 过度使用进程字典

%% 不好的实践:滥用进程字典存储状态
handle_request(Req) ->
    put(request_id, Req#req.id),
    put(user_ctx, Req#req.context),
    process(),
    erase(request_id),
    erase(user_ctx).

进程字典(process dictionary)相当于全局变量,会使代码行为难以预测。

3. 深层模式匹配

%% 深层嵌套的模式匹配难以维护
handle_response({ok, {200, _, {body, {ok, #{result := {data, Items}}}}}}) ->
    process_items(Items);
handle_response({ok, {200, _, {body, {error, Reason}}}}) ->
    {error, Reason};
%% 还有20个类似的子句...

这种代码对数据结构变化极其敏感,一个小改动就可能破坏多处匹配。

三、Erlang重构实战技巧

1. 拆分巨型函数

使用"提取函数"重构法:

%% 重构后的版本
process_order(Order) ->
    with_valid_order(Order, fun(ValidOrder) ->
        with_calculated_price(ValidOrder, fun(Price, VOrder) ->
            with_persisted_order(VOrder, Price, fun() ->
                notify_user(VOrder)
            end)
        end)
    end).

%% 辅助函数(每个约50-100行)
with_valid_order(Order, OnSuccess) ->
    case validate_order(Order) of
        {error, Reason} -> {error, Reason};
        {ok, ValidOrder} -> OnSuccess(ValidOrder)
    end.

with_calculated_price(Order, OnSuccess) ->
    case calculate_price(Order) of
        {error, Reason} -> {error, Reason};
        {ok, Price} -> OnSuccess(Price, Order)
    end.

这种"管道式"处理更清晰,每个步骤都可以独立测试。

2. 用OTP替代裸进程

不要自己管理裸进程,改用gen_server:

%% 重构前
start() ->
    Pid = spawn(fun loop/0),
    register(?MODULE, Pid).

loop() ->
    receive
        {get, Key, From} ->
            From ! {ok, get(Key)},
            loop();
        stop ->
            ok
    end.

%% 重构后
-module(my_server).
-behaviour(gen_server).

% 标准OTP接口
init(_) -> {ok, #{}}.
handle_call({get, Key}, _From, State) ->
    {reply, maps:get(Key, State, undefined), State};
handle_call(stop, _From, State) ->
    {stop, normal, ok, State}.

3. 结构化错误处理

用Either Monad风格替代深层嵌套:

%% 定义错误处理结构
-type either(E, T) :: {error, E} | {ok, T}.

-spec bind(Either :: either(E, T1), Fun :: fun((T1) -> either(E, T2))) -> either(E, T2).
bind({error, E}, _) -> {error, E};
bind({ok, T}, Fun) -> Fun(T).

%% 使用示例
process_order(Order) ->
    bind(validate_order(Order), fun(ValidOrder) ->
        bind(calculate_price(ValidOrder), fun(Price) ->
            bind(save_to_database(ValidOrder, Price), fun(_) ->
                notify_user(ValidOrder)
            end)
        end)
    end).

四、重构策略与最佳实践

1. 测试保护网

重构前必须建立测试覆盖:

%% 使用eunit测试框架
validate_order_test_() ->
    [?_assertEqual({error, invalid_id}, validate_order(#order{id=""})),
     ?_assertMatch({ok, _}, validate_order(#order{id="123", items=[...]}))].

%% 执行测试
run_tests() ->
    eunit:test({module, ?MODULE}).

2. 增量式重构

推荐步骤:

  1. 添加测试
  2. 小步修改(每次改一个点)
  3. 运行测试
  4. 提交版本控制

3. 工具支持

使用rebar3插件:

# 代码格式检查
rebar3 fmt --check

# 死代码检测
rebar3 dialyzer

# 依赖分析
rebar3 xref

五、应用场景与注意事项

典型应用场景

  • 电信系统现代化改造
  • 游戏服务器性能优化
  • 金融交易系统升级

技术优缺点

优点:
✓ 提升代码可读性
✓ 降低维护成本
✓ 提高扩展性

缺点:
✗ 需要充分测试保障
✗ 可能引入短期不稳定
✗ 需要团队达成共识

注意事项

  1. 不要为了重构而重构
  2. 优先重构高频修改的代码
  3. 保持重构小步快跑
  4. 记录重构决策原因

总结

Erlang代码重构是一门艺术,需要在保持系统稳定性的同时渐进式改进。从识别"坏味道"开始,通过提取函数、引入OTP、改进错误处理等手段,可以让遗留系统重获新生。记住:好的代码不是写出来的,而是改出来的。就像修剪盆景,需要耐心和持续的关注。