引言
gen_server、gen_statem、supervisor 这些 OTP 行为(behaviour)把「进程骨架」固定下来,你只需填回调。自定义 behaviour 是把这种「骨架 + 契约」模式复用到你自己的领域——定义回调规范,让团队按统一套路实现模块。本文讲清 behaviour 机制:callback 声明、契约检查、行为实现,以及何时该自定义 behaviour。
前置:/erlang-otp-framework/(OTP 框架)、/erlang-concurrency-actors/(进程与消息)、/erlang-gen-statem-machine/(gen_statem 回调模式)。
目录
- 1. Behaviour:进程的「骨架契约」
- 2. callback 规范:声明回调
- 3. optional_callbacks:可选回调
- 4. 契约检查:编译期校验
- 5. 实现一个自定义 Behaviour
- 6. 与 GenServer 风格的融合
- 7. Behaviour vs Protocol
- 8. 测试行为实现
- 9. 实战:Worker 池 Behaviour
- 10. 速查表与一句话记忆
- 延伸阅读
1. Behaviour:进程的「骨架契约」
1.1 一句话理解
Behaviour = 接口 + 回调约定:定义「你的模块必须实现哪些函数」,OTP 骨架负责调用它们。
-module(gen_server).
-callback init(Args) -> {ok, State} | {ok, State, Timeout}.
-callback handle_call(Request, From, State) -> ... .
1.2 与「协议」的区别
| 维度 | behaviour | protocol |
|---|---|---|
| 场景 | 进程骨架/回调 | 多态分发 |
| 实现 | 模块导出回调 | 每类型实现协议 |
| 检查 | 编译期 warning | 动态解析 |
记忆:behaviour 是进程的骨架契约——定义「实现模块必须提供哪些回调函数」,OTP 骨架负责调用;区别于 protocol 的动态多态分发。
2. callback 规范:声明回调
2.1 定义回调
-module(my_behaviour).
-export([behaviour_info/1]). % 行为规范导出
behaviour_info(callbacks) ->
[ {init, 1}, % init(Args)
{handle_msg, 2}, % handle_msg(Msg, State)
{terminate, 1} ]; % terminate(Reason)
behaviour_info(optional_callbacks) -> [].
2.2 实现模块声明
-module(my_handler).
-behaviour(my_behaviour).
-export([init/1, handle_msg/2, terminate/1]).
init(Args) -> {ok, Args}.
handle_msg(Msg, State) -> {noreply, State}.
terminate(_Reason) -> ok.
记忆:自定义 behaviour 用 behaviour_info(callbacks) 导出回调列表;实现模块用 -behaviour(Name) 声明并导出同名回调。
3. optional_callbacks:可选回调
3.1 声明可选
部分回调不是必须的——实现模块可以不提供:
-module(my_behaviour).
-export([behaviour_info/1]).
behaviour_info(callbacks) ->
[ {init, 1}, {handle_msg, 2}, {handle_info, 2} ];
behaviour_info(optional_callbacks) ->
[ {handle_info, 2} ]. % 可选:不实现也合法
3.2 骨架侧处理缺失
% 骨架调用可选回调前先检查
case erlang:function_exported(Mod, handle_info, 2) of
true -> Mod:handle_info(Msg, State);
false -> {noreply, State}
end.
记忆:optional_callbacks 声明「可不实现」的回调;骨架调用前用 function_exported 检查,缺失走默认分支。
4. 契约检查:编译期校验
4.1 -behaviour 触发检查
实现模块声明 -behaviour(my_behaviour) 后,编译器检查是否导出全部必需回调,缺了会给 warning:
-module(bad_handler).
-behaviour(my_behaviour).
% 漏了 terminate/1
init(Args) -> {ok, Args}.
编译输出:
warning: undefined callback function my_behaviour:terminate/1
4.2 强制错误
想让缺回调变成错误而非 warning,可在编译选项里开 export_all 的对应检查,或 CI 里把 warning 当 error:
erlc +warn_missing_spec +warn_missing_callbacks my_handler.erl
记忆:-behaviour 让编译器检查回调是否齐全,缺失给 warning;CI 用 +warn_missing_callbacks 并开启 warning 即 error,强制契约。
5. 实现一个自定义 Behaviour
5.1 骨架模块(行为实现)
-module(my_behaviour).
-export([behaviour_info/1, start/1, call/2]).
-export([loop/2]).
start(Mod) ->
Pid = spawn(?MODULE, loop, [Mod, Mod:init(startup_args)]),
{ok, Pid}.
loop(Mod, State) ->
receive
{From, Msg} ->
case Mod:handle_msg(Msg, State) of
{noreply, NewState} -> loop(Mod, NewState);
{reply, Reply, NewState} ->
From ! Reply,
loop(Mod, NewState);
{stop, Reason} -> exit(Reason)
end;
stop ->
Mod:terminate(stopped)
end.
call(Pid, Msg) ->
Pid ! {self(), Msg},
receive Reply -> Reply after 5000 -> timeout end.
5.2 契约闭环
骨架(loop)负责:进程循环、消息分发、生命周期
回调(Mod)负责:业务状态与逻辑
骨架与回调通过 behaviour_info 声明的回调契约协作
记忆:自定义 behaviour = 骨架模块(进程循环/消息分发/生命周期) + 回调契约(behaviour_info 声明)+ 实现模块填回调;骨架只信契约,回调只写业务。
6. 与 GenServer 风格的融合
6.1 借鉴 gen_server 的成熟约定
自定义 behaviour 不必从零设计——直接复用 gen_server 的回调风格:
-callback init(Args) -> {ok, State} | {error, Reason}.
-callback handle_call(Request, From, State) ->
{reply, Reply, NewState} | {noreply, NewState}.
-callback handle_cast(Msg, State) -> {noreply, NewState}.
-callback terminate(Reason, State) -> ok.
6.2 复用标准 OTP 骨架
多数「自定义」其实可拆成:
- 业务状态机 → gen_statem
- 带命名注册 → gen_server + start_link
- 通用缓存/数据 → ETS + gen_server
记忆:先问能不能直接用 OTP 自带行为(gen_server/gen_statem/supervisor);真正需要自定义时,回调风格照抄 gen_server 约定,降低团队学习成本。
7. Behaviour vs Protocol
7.1 什么时候用哪个
| 场景 | 用 behaviour | 用 protocol |
|---|---|---|
| 进程骨架 | ✓ | ✗ |
| 同一模块多实现 | ✓ | ✗ |
| 同一类型多操作分发 | ✗ | ✓ |
| 数据类型多态 | ✗ | ✓ |
7.2 简单规则
「进程怎么跑」→ behaviour
「数据怎么处理」→ protocol / function clause
记忆:behaviour 管「进程骨架的回调契约」,protocol 管「不同类型上的操作分发」;进程场景用 behaviour、数据多态用 protocol。
8. 测试行为实现
8.1 测试回调单元
回调就是纯函数,可单测:
-module(my_handler_tests).
-include_lib("eunit/include/eunit.hrl").
init_returns_ok_test() ->
?assertMatch({ok, #{}}, my_handler:init([])).
handle_msg_accumulates_test() ->
{noreply, S1} = my_handler:handle_msg({add, 1}, #{n => 0}),
?assertEqual(#{n => 1}, S1).
8.2 测试行为骨架
用 EUnit 直接测骨架的循环行为(配合 Meck/仿真):
test_start_and_call_test() ->
{ok, Pid} = my_behaviour:start(fake_handler),
Reply = my_behaviour:call(Pid, ping),
?assertEqual(pong, Reply).
记忆:回调函数就是纯函数可 EUnit 直测;骨架用 start + call 集成测试,fake_handler 提供最小实现。
9. 实战:Worker 池 Behaviour
9.1 需求
一组 worker 进程,对外提供 run(Work),失败自动重启。
9.2 设计
%% worker_behaviour.erl
-behaviour 契约:
-callback run(Work) -> ok | {error, Reason}.
-callback on_success(Result, State) -> State.
-callback on_error(Error, State) -> State.
%% 骨架:spawn worker、监控退出、调用回调
start(Mod) ->
supervisor:start_link(worker_sup, [{Mod, worker}]) % 复用 supervisor 重启
...
9.3 复用 OTP
真正落地方案:worker 池 = supervisor + gen_server worker,自定义 behaviour 只声明业务回调(run/on_success/on_error)。
记忆:Worker 池实战 = supervisor(重启策略)+ gen_server worker(进程)+ 自定义 behaviour 声明业务回调(run/on_success/on_error)——骨架复用 OTP,自定义只管契约。
10. 速查表与一句话记忆
| 环节 | 关键点 |
|---|---|
| 声明回调 | behaviour_info(callbacks) |
| 可选回调 | behaviour_info(optional_callbacks) |
| 实现声明 | -behaviour(Name) |
| 契约检查 | 编译 warning + CI 强制 |
| 骨架职责 | 进程循环/消息分发 |
| 回调职责 | 业务逻辑/状态 |
| 替代 | 优先复用 gen_server/gen_statem |
| 测试 | EUnit 测回调 + 骨架 |
一句话记忆:自定义 OTP behaviour = 骨架契约——用 behaviour_info(callbacks) 声明「实现模块必须导出哪些回调」,optional_callbacks 声明可选的,实现模块 -behaviour(Name) 声明后编译器检查契约(缺失给 warning);骨架负责进程循环/消息分发/生命周期,回调只写业务状态;多数场景先复用 OTP 自带行为(gen_server/gen_statem/supervisor),真需自定义时回调风格照抄 gen_server 约定;回调是纯函数可 EUnit 直测,骨架用 fake 实现集成测试;「进程怎么跑」用 behaviour、「数据多态」用 protocol。
延伸阅读
- /erlang-otp-framework/ — OTP 框架与 GenServer
- /erlang-gen-statem-machine/ — gen_statem 回调模式
- /erlang-concurrency-actors/ — 进程与消息传递
- /erlang-ets-caching/ — ETS 数据与进程协作
- /elixir-testing-property/ — 测试方法论
- [[scala]] — Actor 模型跨语言对比
- Erlang OTP 设计原则
- Erlang behaviour 文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。