Обзор

Материал из Erlang по-русски.

Оригинал этой статьи находится по адресу Overview

Это страница для перевода Erlang/OTP Design principles.

Автор: Mirrorer
Дата: 29.11.2006
Версия: 1.01



OTP Design Principles – это набор принципов по структурированию кода на языке Erlang в терминах процессов, модулей и директорий.


Содержание

Деревья контроля

Основной концепцией в Erlang/OTP является дерево контроля (supervision tree). Это модель структурирования процессов, основанная на идее рабочих и контролеров.

На рисунке, приведенном выше, квадратными блоками представлены контролеры, кругами – рабочие.

Поведения

В дереве контроля многие процессы имеют схожую структуру, следуют похожим паттернам. Например, контролеры очень похожи по своей структуре. Единственное отличие между ними – дочерние процессы, которые они контролируют. Также, многие из рабочих являются серверами в клиент-серверной модели, конечными автоматами или обработчиками событий, такими как логгер ошибок.

Поведения(Behaviours) – это формализация таких общих паттернов. Идея заключается в том, чтобы разделить код в процессе на общую часть (модуль поведения) и специализированную часть (модуль обратного вызова(callback module))

Модуль поведения является частью Erlang/OTP. Для того, чтобы реализовать процесс как контролер, пользователю необходимо только реализовать модуль обратного вызова, который должен экспортировать предопределенное множество функций (функций обратного вызова(callback functions)).

Пример продемонстрирует, как код может быть разделен на общую и специализированную части. Предположим, следующий код (написанный на чистом 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 или любой другой модуль обратного вызова.

Код, написанный без использования поведений, может быть более эффективным, но платой за увеличение эффективности будет потеря общности. Возможность управлять всеми приложениями в системе одинаковым способом очень важна.

Использование поведений делает чтение и понимание кода другими программистами легче. Структуры Ad hoc, возможно более эффективные, всегда сложнее понимать.

Модуль server соотвествует, со значительными упрощениями, поведению gen_server из Erlang/OTP.

Стандартными поведениями в Erlang/OTP являются:

gen_server Для реализации клиент-серверной модели взаимодействия.

gen_fsm Для реализации конечных автоматов.

gen_event Для реализации обработки событий.

supervisor Для реализации контролера в дереве контроля.

Компилятор понимает атрибут модуля -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 называются приложениями (applications). Примерами приложений Erlang/OTP являются Mnesia, которая содержит в себе все необходимое для программирования баз данных, и Debugger, используемый для отладки Erlang программ. Минимальная система, основанная на Erlang/OTP, состоит из приложений Kernel и STDLIB.

Концепция приложений относится и к структуре программы (процессам), и к структуре директорий (модулям).

Приложение простейшего типа не содержит ни одного процесса, а состоит из набора функциональных модулей. Такие приложения называются библиотечными приложениями(library application).

Приложение с процессами проще всего реализовать как дерево контроля, использующее стандартные поведения.

Как создавать приложения – описано в Приложения.

Релизы

Релиз – это полноценная система, сделанная на основе некоторого подмножества приложений the Erlang/OTP и набора написанных пользователем приложений.

Создание релизов описано в Релизы.

Как устанавливать релиз на нужную систему, описано в главе о развертывании систем.

Управление релизами

Управление релизами(Release handling) – это обновление или установка более старой версии релиза на (возможно) работающей системе. Как это сделать, описано в Управление релизами.