自定义 OTP Behaviour 实战:Callback 规范与行为封装

自定义 OTP behaviour 实战:behaviour 是什么、callback 规范(callback/optional_callbacks)、行为实现的契约检查、构建自定义 GenServer 风格行为、错误处理与调用约定、行为 vs 协议的选型、测试行为实现、实战案例(Worker/Registry)。

引言

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:进程的「骨架契约」

1.1 一句话理解

Behaviour = 接口 + 回调约定:定义「你的模块必须实现哪些函数」,OTP 骨架负责调用它们。

-module(gen_server).
-callback init(Args) -> {ok, State} | {ok, State, Timeout}.
-callback handle_call(Request, From, State) -> ... .

1.2 与「协议」的区别

维度behaviourprotocol
场景进程骨架/回调多态分发
实现模块导出回调每类型实现协议
检查编译期 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 文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「erlang」更多文章

  1. Phoenix Channels 实时通信实战:WebSocket 与 PubSub 深入
  2. Mix 工具链与 Elixir 工程化实战
  3. LiveView 进阶实战:状态管理、并发与性能优化