原始链接:https://blog.zhustec.me/posts/erlang-otp-1-callback-based-on-behaviour

OTP 是什么

OTP 的全称是开源电信平台 (Open Telecom Platform),但是它的实际作用却不像它的名字一样只是用做电信平台,它是 Erlang 中的一套用于方便用户编写高容错性系统的框架。如果说 Erlang 的伟大之处一半来自于它的并发和分布式,那么另一半就来自于 OTP 框架。

为什么需要 OTP

在编写 Erlang 程序时,有很大一部分代码都是重复造轮子,重复编写的,而 OTP 则是提取出这些代码中的通用部分,而使用户只需要编写特定代码部分而不需要重复编写通用部分的代码。

传统的代码

比如一个简单的客户进程与服务进程通讯的例子:服务进程仅仅打印出客户进程发送过来的消息

-module(server).

-export ([start/0, req/1, loop/0]).

start() ->
register(?MODULE, spawn_link(?MODULE, loop, [])). req(Req) ->
?MODULE ! {self(), Req}. loop() ->
receive
{_, stop} -> ok;
{_, Msg } ->
io:format("~p~n", [{ok, Msg}]),
loop()
end.

而现在我需要编写另外一个程序,服务进程不是打印出客户进程的请求,而是返回给客户进程,那么我需要再写一个模块

-module(server2).

-export([start/0, req/1, loop/0]).

start() ->
register(?MODULE, spawn_link(?MODULE, loop, [])). req(Req) ->
?MODULE ! {self(), Req}. loop() ->
receive
{_, stop} -> ok;
{From, Msg} ->
From ! {ok, Msg},
loop()
end.

可以看出上面两段程序作为简单的客户进程与服务进程交互的例子具有很多的相似部分。

  1. 服务进程启动时都需要使用 swapn_link 创建并注册到当前模块名的原子
  2. 提供了包装了访问服务器请求的接口 req 函数
  3. 服务进程最终进入无限循环 loop 中接收消息直到遇到 stop 命令

而两段程序的差别仅仅是在无限循环 loop 中所接受的请求及对应的处理方式。

程序的代码可以分成两个部分

  • 通用部分:在许多程序中都能使用的代码
  • 特定部分:只在个别程序中使用的代码

所以我们可以将上面的程序中的代码分成两个部分

  • 通用部分:start 函数和 loop 函数及期中的 receive 块
  • 特定部分:rpc 客户进程请求和 receive 的消息

既然上面两段程序有许多通用的代码,我们可以考虑使用代理模块来剥离出通用部分。

使用代理模块剥离出通用部分

将第一个程序修改为

-module(server).

-export([start/0, req/1, handle_req/1]).

start() ->
proxy:start_link(?MODULE). req(Req) ->
proxy:req(?MODULE, Req). handle_req(Msg, _From) ->
io:format("~p~n", [{ok, Msg}]).

而代理模块看起来就像这样

-module(proxy).

-export([start_link/1, req/2, loop/1]).

start_link(Mod) ->
register(Mod, spawn_link(?MODULE, loop, [Mod])). req(Mod, Req) ->
Mod ! {Req, self()}. loop(Mod) ->
receive
{Msg, From} ->
case Mod:handle_msg(Msg, From) of
stop ->
ok;
_ ->
loop(Mod)
end
end.

而此时,第二个程序就可以修改为

-module(server2).

-export([start/0, req/1, handle_req/1]).

start() ->
proxy:start_link(?MODULE). req(Req) ->
proxy:req(?MODULE, Req). handle_req(Msg, _From) ->
From ! {ok, Msg}.

或者,可以再写出一个程序。

-module(server3).

-export([start/0, print/1, stop/0, handle_req/1]).

start() ->
proxy:start_link(?MODULE). print(String) ->
proxy:req(?MODULE, {print, String}). stop() ->
proxy:req(?MODULE, stop). handle_req(stop, _) ->
io:format("Time to die.~n"),
stop;
handle_req({print, String}, _) ->
io:format(String);
ok;
handle_req(_, _) ->
stop.

通过使用代理模块将程序的通用部分剥离出来的方式,简化了程序的编写。

有些人可能会问,没看出来使用代理模块后的程序代码数量减少。实际上,我这里只是写的一个简单的代理模块,而且服务程序也是简单的服务程序。在生产应用中,服务程序的启动不会是只是简单启动一个新进程运行一个无限循环函数那么简单,而是要进行若干操作并初始化服务状态,所处理的消息也包括各种同步消息、异步消息及来自其他进程的消息,另外还要处理各种异常和进行热代码更新。这样服务程序之间的通用部分与特定部分的代码比例将非常高,这个时候使用代理模块剥离出通用部分的代码将显得十分有效。

OTP 就是一套复杂的代理模块集合

上面通过使用代理模块剥离出了许多客户-服务交互程序的通用部分代码,而 OTP 就是一套复杂的、具有高容错性的代理模块的集合,它就将通用部分包装并隐藏起来,而通过调用特定程序的回调函数来执行特定部分代码。

其实上面的代理模块是我对 gen_server 模块的拙劣模仿

OTP 的简单实现原理

上面提到,OTP 将通用代码剥离出来放进代理模块中,而特定代码则使用回调函数实现。

回调函数

在上面的代码中,代理模块里的 loop 函数在接收到消息后调用 Mod:handle_msg(Msg, From) 调用用户实现的回调函数,这行语句要求用户的模块必须实现并导出了 handle_msg/2 函数,但是如果用户没有实现这个函数的话,运行时就会崩溃掉。因此,需要一种方式通知用户,如果你要让我代理你,你需要实现我所要求的回调函数,这种方式就是使用 behaviour 属性。

behaviour 属性

在 Erlang 中可以为模块添加自定义的属性。

-module(test_tag).

-tag1(tagname1).
-tag2(tagname2).
-tag3(tagname3). % Run in Erlang Shell
%
% c(test_tag).
%
% test_tag:module_info(attributes).
% => [{vsn, [blablabla]},
% => {tag1, tagname1},
% => {tag2, tagname2},
% => {tag3, tagname3}]

所以可以使用 -behaviour(Behaviour). 添加一个 behaviour 的属性

-module(test_be).

-behaviour(be_example).

% Run in Erlang Shell
%
% c(test_be).
%
% test_be:module_info(attributes).
% => [{vsn, [blablabla]},
% => {behaviour, [be_example]}]

Behaviour 必须是已经加载的或者在加载路径里已经编译好的模块名,它的意思是:当前模块在行为上类似于 be_example

实际上,behaviour 的作用是用来确保一个模块必须导出一系列被称作为回调函数的函数。比如 test_be 模块拥有 {behaviour, be_example} 的属性,则 test_be 模块必须导出 be_example 模块所要求的回调函数,如果没有导出相关的函数,在编译时将会报出警告。

一个模块可以具有多个 behaviour 属性,这个模块必须具有这些模块所指定的所有回调函数。

-module(test_be).

-behaviour(application).
-behaviour(supervisor). % Run in Erlang Shell
%
% c(test_be).
% Warning: undefined callback function start/2 (behaviour 'application')
% Warning: undefined callback function stop/1 (behaviour 'application')
% Warning: undefined callback function init/1 (behaviour 'supervisor')
%
% test_be:module_info(attributes).
% => [{vsn, [blablabla]},
% => {behaviour, [application]},
% => {behaviour, [supervisor]}]

上面的例子中,当前模块必须导出模块 application 所要求的回调函数,同时也必须导出模块 supervisor 所要求的回调函数。当然在实际使用时不建议这么使用,这里只是作为例子来解释。

定义 Behaviour

上面说到,Behaviour 实际上就是声明了一些必须被导出的函数,声明的方式有两种

定义并导出 behaviour_info/1 函数

方法的返回值是由函数名,函数参数个数组成的元组的列表

-module(be_example).

-export([behaviour_info/1]).

-spec behaviour_info(callbacks) -> [{FunName, Arity}] when
FunName :: atom(),
Arity :: integer(). behaviour_info(callbacks) ->
[{init, 1},
{handle_msg, 2},
{handle_info, 2}].

当编译器编译到语句 -behaviour(Behaviour). 时,编译器会调用

-Behaviour:behaviour_info(callbacks).

然后比较结果与模块的导出函数,如果有 callbacks 没有被导出,就会产生一个警告。

定义 callback 属性

这个属性会自动生成 behaviour_info/1 函数

-module(be_example).

-callback init(Args :: term()) ->
{ok, State :: term()} | {error, Reason :: term()}. -callback handle_msg(Msg :: term(), From :: pid()) ->
{ok, Reply :: term()} | {stop, Reason :: term()}. -callback handle_info(Info :: term()) ->
ok | {stop, Reason :: term()}.

打开 Erlang Shell 编译并使用这个模块

c(be_example).

be_example:behaviour_info(callbacks).
% => [{init,1},{handle_msg,2},{handle_info,1}] be_example:module_info(attributes).
% => [{vsn,[blablabla]},
% => {callback,[{ {init,1},
% => [{type,3,'fun',
% => [blablabla]}]}]}]}]

建议使用第二种方式,因为从上面的例子就能看出来了使用 callbacks 属性可以定义回调函数的参数类型和返回值类型。通常只能在比较老的代码中才能看到第一种方式了,而现在标准库使用的都是第二种方法。

Erlang/OTP:基于Behaviour的回调函数的更多相关文章

  1. PHP 中的回调函数

    回调函数就是在主进程执行过程中,满足某个条件后,跳转到预先设置好的函数中去执行的一种函数. 举例说明: 张三到一个水果店买苹果,刚好苹果卖完了,于是张三在店员那里留下了自己的电话,一个小时后店里有货了 ...

  2. CXF 入门:创建一个基于WS-Security标准的安全验证(CXF回调函数使用,)

    http://jyao.iteye.com/blog/1346547 注意:以下客户端调用代码中获取服务端ws实例,都是通过CXF 入门: 远程接口调用方式实现 直入正题! 以下是服务端配置 ==== ...

  3. javascript . 04 匿名函数、递归、回调函数、对象、基于对象的javascript、状态和行为、New、This、构造函数/自定义对象、属性绑定、进制转换

    匿名函数:   没有名字的函数,函数整体加小括号不报错, 函数调用 : a:直接调用 (function (){函数体}) ( ) ; b:事件绑定 document.onlick = functio ...

  4. 基于Lwip协议栈中独立模式下回调函数的使用

    一.使用Lwip协议独立模式开发 最近在STM32F4上边移植了Lwip,Lwip是一个小型开源的TCP/IP协议栈,有无操作系统的支持都可以运行.我当前只测试了TCP Server功能,然后对TCP ...

  5. 基于委托的C#异步编程的一个小例子 带有回调函数的例子

    我创建的是一个winform测试项目:界面如下: 设置: 下面是代码: using System; using System.Collections.Generic; using System.Com ...

  6. Erlang/OTP设计原则(文档翻译)

    http://erlang.org/doc/design_principles/des_princ.html 图和代码皆源自以上链接中Erlang官方文档,翻译时的版本为20.1. 这个设计原则,其实 ...

  7. Erlang OTP学习:supervisor [转]

    转自: http://diaocow.iteye.com/blog/1762895 今天细致的看了下supervisor,现在做个总结: 其中,方块代表supervisor process,它的功能很 ...

  8. erlang OTP gen_server 图解分析

    http://www.hoterran.info/otp-gen_server-sourcecode 在阅读erlang的otp源码gen_server.erl的时候,一直想写点什么,用一种最好的方式 ...

  9. Erlang OTP编程初体验——gen_server和行为模式

    http://blog.sina.com.cn/s/blog_3fe961ae0101k4p6.html 行为模式其实非常类似于面向对象语言中的接口,至少笔者是这么理解的.OTP行为模式将一些反复出现 ...

随机推荐

  1. C语言面试题5

    C语言面试宝典 第一部分:基本概念及其它问答题 1.关键字static的作用是什么? 这个简单的问题很少有人能回答完全.在C语言中,关键字static有三个明显的作用: 1). 在函数体,一个被声明为 ...

  2. zz 史上最全--各银行借记卡的年费、小额管理费、转账费等!

    史上最全--各银行借记卡的年费.小额管理费.转账费等! 发布时间:2015-01-14 17:28:10 还在迷茫借记卡自费的菜主儿们~菜菜特别整理关于各银行借记卡.存折账户等的年费.小额管理费.转账 ...

  3. WPF中控制窗口显示位置的三种方式

    首先新建一个WPF工程,在主界面添加一个按钮,并给按钮添加点击事件button1_Click,然后新建一个用于测试弹出位置的窗口TestWindow.1.在屏幕中间显示,设置window.Window ...

  4. Java JDBC连接Oracle

    1. 安装Oracle数据库,我这里使用的是Oracle 12c 2. 创建Java工程 connection-oracle 注意:使用的JavaSE-1.8 3. 在Oracle的安装目录里,将dj ...

  5. Juery 实现淡出 淡现效果

    HTML: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w ...

  6. 浏览器同源策略,跨域请求jsonp

    浏览器的同源策略 浏览器安全的基石是"同源政策"(same-origin policy) 含义: 1995年,同源政策由 Netscape 公司引入浏览器.目前,所有浏览器都实行这 ...

  7. 关于android webview 设置cookie的问题

    转自:http://blog.csdn.net/encienqi/article/details/7912733 我们在android中访问网络经常会用到Apache的HttpClient,用此类去访 ...

  8. 2018 icpc 徐州网络赛 F Features Track

    这个题,我也没想过我这样直接就过了 #include<bits/stdc++.h> using namespace std; ; typedef pair<int,int> p ...

  9. spring eureka required a bean of type 'com.netflix.discovery.DiscoveryClient' that could not be found.

    spring在集成第三方过程很容易出现类名相同,且基本作用相同的类.这样给初学者带来一定的困惑. 导致用错类而出现以下问题. required a bean of type 'com.netflix. ...

  10. C++中reinterpret_cast、const_cast、static_cast、dynamic_cast的作用与区别

    1.reinterpret_cast 作用及原理:将一个类型的指针,转换为另一个类型的指针,这种转换不用修改指针变量值数据存放格式(不改变指针变量值),只需在编译时重新解释指针的类型就可以,当然他也可 ...