1  概述

1 概述

**OTP 设计原则**定义了如何在进程、模块和目录方面构建 Erlang 代码。

Erlang/OTP 中的一个基本概念是**监督树**。这是一个基于**工作者**和**监督者**概念的进程结构模型。

  • 工作者是执行计算的进程,即它们进行实际工作。
  • 监督者是监控工作者行为的进程。如果发生错误,监督者可以重启工作者。
  • 监督树是代码的层次结构安排,分为监督者和工作者,这使得设计和编程容错软件成为可能。

在下图中,方形框代表监督者,圆形框代表工作者。

IMAGE MISSING

图 1.1:   监督树

在监督树中,许多进程具有相似的结构,它们遵循相似的模式。例如,监督者在结构上相似。它们之间唯一的区别在于它们监督哪些子进程。许多工作者是服务器-客户端关系中的服务器、有限状态机或事件处理程序。

**行为**是对这些常见模式的正式化。其思想是将进程的代码划分为通用部分(行为模块)和特定部分(**回调模块**)。

行为模块是 Erlang/OTP 的一部分。要实现诸如监督者之类的进程,用户只需实现回调模块,该模块需要导出预定义的函数集,即**回调函数**。

以下示例说明了如何将代码划分为通用部分和特定部分。考虑以下代码(用纯 Erlang 编写)用于一个简单的服务器,它跟踪多个“通道”。其他进程可以通过调用函数alloc/0free/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.erlch2.erl中,channels/0alloc/1free/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}

Erlang/OTP 带有许多组件,每个组件实现某些特定功能。组件用 Erlang/OTP 术语称为**应用程序**。Erlang/OTP 应用程序的例子是 Mnesia,它包含了编写数据库服务的全部内容,以及 Debugger,它用于调试 Erlang 程序。基于 Erlang/OTP 的最小系统包含以下两个应用程序

  • Kernel - 运行 Erlang 所需的功能
  • STDLIB - Erlang 标准库

应用程序概念既适用于程序结构(进程),也适用于目录结构(模块)。

最简单的应用程序没有进程,而是由一系列功能模块组成。这种应用程序称为**库应用程序**。STDLIB 是库应用程序的一个例子。

最容易使用标准行为以监督树的形式实现具有进程的应用程序。

如何在Applications中描述如何编程应用程序。

**发布**是一个完整的系统,由 Erlang/OTP 应用程序的子集和一组用户特定应用程序组成。

如何在Releases中描述如何编程发布。

如何在目标环境中安装发布在第 2 节系统原则中关于目标系统的部分进行了描述。

**发布处理**是在(可能)运行的系统中对发布的不同版本进行升级和降级。如何在Release Handling中描述了这一点。