一、为什么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. 增量式重构
推荐步骤:
- 添加测试
- 小步修改(每次改一个点)
- 运行测试
- 提交版本控制
3. 工具支持
使用rebar3插件:
# 代码格式检查
rebar3 fmt --check
# 死代码检测
rebar3 dialyzer
# 依赖分析
rebar3 xref
五、应用场景与注意事项
典型应用场景
- 电信系统现代化改造
- 游戏服务器性能优化
- 金融交易系统升级
技术优缺点
优点:
✓ 提升代码可读性
✓ 降低维护成本
✓ 提高扩展性
缺点:
✗ 需要充分测试保障
✗ 可能引入短期不稳定
✗ 需要团队达成共识
注意事项
- 不要为了重构而重构
- 优先重构高频修改的代码
- 保持重构小步快跑
- 记录重构决策原因
总结
Erlang代码重构是一门艺术,需要在保持系统稳定性的同时渐进式改进。从识别"坏味道"开始,通过提取函数、引入OTP、改进错误处理等手段,可以让遗留系统重获新生。记住:好的代码不是写出来的,而是改出来的。就像修剪盆景,需要耐心和持续的关注。
评论