1 概述
**OTP 设计原则**定义了如何在进程、模块和目录方面构建 Erlang 代码。
1.1 监督树
Erlang/OTP 中的一个基本概念是**监督树**。这是一个基于**工作者**和**监督者**概念的进程结构模型。
- 工作者是执行计算的进程,即它们进行实际工作。
- 监督者是监控工作者行为的进程。如果发生错误,监督者可以重启工作者。
- 监督树是代码的层次结构安排,分为监督者和工作者,这使得设计和编程容错软件成为可能。
在下图中,方形框代表监督者,圆形框代表工作者。
1.2 行为
在监督树中,许多进程具有相似的结构,它们遵循相似的模式。例如,监督者在结构上相似。它们之间唯一的区别在于它们监督哪些子进程。许多工作者是服务器-客户端关系中的服务器、有限状态机或事件处理程序。
**行为**是对这些常见模式的正式化。其思想是将进程的代码划分为通用部分(行为模块)和特定部分(**回调模块**)。
行为模块是 Erlang/OTP 的一部分。要实现诸如监督者之类的进程,用户只需实现回调模块,该模块需要导出预定义的函数集,即**回调函数**。
以下示例说明了如何将代码划分为通用部分和特定部分。考虑以下代码(用纯 Erlang 编写)用于一个简单的服务器,它跟踪多个“通道”。其他进程可以通过调用函数alloc/0和free/1分别分配和释放通道。
-module(ch1). -export([start/0]). -export([alloc/0, free/1]). -export([init/0]). start() -> spawn(ch1, init, []). alloc() -> ch1 ! {self(), alloc}, receive {ch1, Res} -> Res end. free(Ch) -> ch1 ! {free, Ch}, ok. init() -> register(ch1, self()), Chs = channels(), loop(Chs). loop(Chs) -> receive {From, alloc} -> {Ch, Chs2} = alloc(Chs), From ! {ch1, Ch}, loop(Chs2); {free, Ch} -> Chs2 = free(Ch, Chs), loop(Chs2) end.
服务器的代码可以重写为一个通用部分server.erl
-module(server). -export([start/1]). -export([call/2, cast/2]). -export([init/1]). start(Mod) -> spawn(server, init, [Mod]). call(Name, Req) -> Name ! {call, self(), Req}, receive {Name, Res} -> Res end. cast(Name, Req) -> Name ! {cast, Req}, ok. init(Mod) -> register(Mod, self()), State = Mod:init(), loop(Mod, State). loop(Mod, State) -> receive {call, From, Req} -> {Res, State2} = Mod:handle_call(Req, State), From ! {Mod, Res}, loop(Mod, State2); {cast, Req} -> State2 = Mod:handle_cast(Req, State), loop(Mod, State2) end.
以及一个回调模块ch2.erl
-module(ch2). -export([start/0]). -export([alloc/0, free/1]). -export([init/0, handle_call/2, handle_cast/2]). start() -> server:start(ch2). alloc() -> server:call(ch2, alloc). free(Ch) -> server:cast(ch2, {free, Ch}). init() -> channels(). handle_call(alloc, Chs) -> alloc(Chs). % => {Ch,Chs2} handle_cast({free, Ch}, Chs) -> free(Ch, Chs). % => Chs2
注意以下几点:
- 中的代码server可以重复使用来构建许多不同的服务器。
- 服务器名称,在本例中是原子ch2,对客户端函数的用户隐藏。这意味着可以更改名称而不会影响它们。
- 协议(发送到服务器和从服务器接收的消息)也被隐藏。这是良好的编程实践,允许在不改变使用接口函数的代码的情况下改变协议。
- 的功能可以扩展server,而无需改变ch2或任何其他回调模块。
在ch1.erl和ch2.erl中,channels/0、alloc/1和free/2的实现故意省略了,因为它们与示例无关。为了完整起见,下面给出了编写这些函数的一种方法。这只是一个示例,一个真实的实现必须能够处理诸如分配通道不足等情况。
channels() -> {_Allocated = [], _Free = lists:seq(1,100)}. alloc({Allocated, [H|T] = _Free}) -> {H, {[H|Allocated], T}}. free(Ch, {Alloc, Free} = Channels) -> case lists:member(Ch, Alloc) of true -> {lists:delete(Ch, Alloc), [Ch|Free]}; false -> Channels end.
没有使用行为编写的代码可能效率更高,但效率的提高是以通用性为代价的。以一致的方式管理系统中所有应用程序的能力非常重要。
使用行为还使阅读和理解其他程序员编写的代码变得更容易。即兴的编程结构虽然可能效率更高,但总是更难理解。
模块对应于server,很大程度上简化了 Erlang/OTP 行为gen_server。
标准 Erlang/OTP 行为是:
-
用于实现客户端-服务器关系的服务器
-
用于实现状态机
-
用于实现事件处理功能
-
用于在监督树中实现监督者
编译器理解模块属性-behaviour(Behaviour)并发出有关缺少回调函数的警告,例如:
-module(chs3). -behaviour(gen_server). ... 3> c(chs3). ./chs3.erl:10: Warning: undefined call-back function handle_call/3 {ok,chs3}
1.3 应用程序
Erlang/OTP 带有许多组件,每个组件实现某些特定功能。组件用 Erlang/OTP 术语称为**应用程序**。Erlang/OTP 应用程序的例子是 Mnesia,它包含了编写数据库服务的全部内容,以及 Debugger,它用于调试 Erlang 程序。基于 Erlang/OTP 的最小系统包含以下两个应用程序
- Kernel - 运行 Erlang 所需的功能
- STDLIB - Erlang 标准库
应用程序概念既适用于程序结构(进程),也适用于目录结构(模块)。
最简单的应用程序没有进程,而是由一系列功能模块组成。这种应用程序称为**库应用程序**。STDLIB 是库应用程序的一个例子。
最容易使用标准行为以监督树的形式实现具有进程的应用程序。
如何在Applications中描述如何编程应用程序。
1.4 发布
**发布**是一个完整的系统,由 Erlang/OTP 应用程序的子集和一组用户特定应用程序组成。
如何在Releases中描述如何编程发布。
如何在目标环境中安装发布在第 2 节系统原则中关于目标系统的部分进行了描述。
1.5 发布处理
**发布处理**是在(可能)运行的系统中对发布的不同版本进行升级和降级。如何在Release Handling中描述了这一点。