Добрый день.
Пара слов о себе сначала. Я пишу на Erlang-е около 10 лет и приветствую появившиеся в последнее время схемы и диаграммы. Но я помню какой переворот в моем коде вызвало применение поведений, и думаю что это интересная тема для сложных продуктов.
Зачем нужны поведения? Поведение — суть определение интерфейса. Установка контракта между вызывающей стороной и имплементацией. Ну и все что из этого вытекает в случаях обычного определения интерфейса класса. Только в этом случае мы определяем интерфейс модуля.
Модуль может выполнять больше, чем одно поведение, но надо аккуратно смотреть, чтобы поведения не пересекались.
Если поведения декларируют функцию, совпадающую по имени и количеству параметров, то при компиляции появляется логичное предупреждение conflicting behaviours.
Синтаксическая сторона очень проста. Я положу код для примера и потом продолжу описание.
Код поведения:
И два модуля выполняющие эти поведения:
Как видно, поведение декларирует 3 функции, одна из которых опциональна. Опциональная функция обычно заворачивается в безопасный обработчик в теле самого поведения, который проверяет наличие функции в выполняющем модуле. Декларация функций включает в себя спецификацию, и рекоммендуется спецификацию делать максимально детализированной. Это, в свою очередь, облегчит статическую проверку кода с помощю dialyzer-а.
В имплементирующих поведение модулях, а они в примере почти идентичны, кроме функции default, видно что декларация функций повторяет те, что находятся в определении поведения. В реальности декларация может быть любым сабсетом оригинальной функции, если конкретная реализация не создает все возможные сценарии.
Более интересен случай это создание цепочки поведений, когда сам модуль поведения в свою очередь выполняет другое поведение сам. Пример можно посмотреть в часности в стандартной библиотеке Erlang, когда поведение supervisor в свою очередь выполняет поведение gen_server. В этом случае код делится на две части — первая выполняет свои контрактные обязательства, вторая предоставляет служебные функции для других модулей по новому контракту.
В функциональном коде далеко не всегда есть необходимость в определении новых поведений. И критерий необходимости будет прост — два и больше модуля имеющие одинаковую семантику или роль, требуют определения интерфейса и унификации. Затраченое время облегчит и тестирование и будущее расширение кода. Потому-что если есть 2-3 модуля с одной ролью, высок шанс появления еще нескольких.
Пара слов о себе сначала. Я пишу на Erlang-е около 10 лет и приветствую появившиеся в последнее время схемы и диаграммы. Но я помню какой переворот в моем коде вызвало применение поведений, и думаю что это интересная тема для сложных продуктов.
Зачем нужны поведения? Поведение — суть определение интерфейса. Установка контракта между вызывающей стороной и имплементацией. Ну и все что из этого вытекает в случаях обычного определения интерфейса класса. Только в этом случае мы определяем интерфейс модуля.
Модуль может выполнять больше, чем одно поведение, но надо аккуратно смотреть, чтобы поведения не пересекались.
Если поведения декларируют функцию, совпадающую по имени и количеству параметров, то при компиляции появляется логичное предупреждение conflicting behaviours.
Синтаксическая сторона очень проста. Я положу код для примера и потом продолжу описание.
Код поведения:
-module(sample_behavoiur).
-export([default/2]).
-callback init(Args :: list())->
{ok, State :: term()}.
-callback action(State :: term())->
{ok, ActionResult :: term(), State::term()} | {error, ErrorInfo :: term() }.
-callback default(State :: term() )->
{ok, DefaultResult :: term() }.
-optional_callbacks([default/1]).
-spec default(Mod :: atom(), State :: term())-> {ok, DefaultResult :: term() }.
default(Mod, State)->
case erlang:function_exported(Mod, default, 1) of
true ->
Mod:default(State);
false ->
{ok, default}
end.
И два модуля выполняющие эти поведения:
-module(implement_1).
-behaviour(sample_behavoiur).
-export([init/1,
action/1
]).
-record(state,{list :: [integer()]}).
init(Args) ->
{ok, #state{list = Args}}.
action(State = #state{list = []})->
{ok, empty, State};
action(State = #state{list = [Head|Rest]})->
{ok, Head, State#state{list = Rest}}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
simple_test()->
{ok, Opaq1} = implement_1:init([1,2,3]),
{ok, 1, Opaq2} = implement_1:action(Opaq1),
{ok, 2, Opaq3} = implement_1:action(Opaq2),
{ok, 3, Opaq4} = implement_1:action(Opaq3),
{ok, empty, Opaq5} = implement_1:action(Opaq4),
{ok, default} = sample_behavoiur:default(implement_1, Opaq4).
-endif.
-module(implement_2).
-behaviour(sample_behavoiur).
-export([init/1,
action/1,
default/1
]).
-record(state,{list :: [integer()]}).
init(Args) ->
{ok, #state{list = Args}}.
action(State = #state{list = []})->
{ok, empty, State};
action(State = #state{list = [Head|Rest]})->
{ok, Head, State#state{list = Rest}}.
default(_State = #state{list = []})->
{ok, empty};
default(_State = #state{list = [Head|_]})->
{ok, Head}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
simple_test()->
{ok, Opaq1} = implement_2:init([1,2,3]),
{ok, 1, Opaq2} = implement_2:action(Opaq1),
{ok, 2} = sample_behavoiur:default(implement_2, Opaq2),
{ok, 2, Opaq3} = implement_2:action(Opaq2),
{ok, 3, Opaq4} = implement_2:action(Opaq3),
{ok, empty, Opaq5} = implement_2:action(Opaq4),
{ok, empty} = sample_behavoiur:default(implement_2, Opaq4)
-endif.
Как видно, поведение декларирует 3 функции, одна из которых опциональна. Опциональная функция обычно заворачивается в безопасный обработчик в теле самого поведения, который проверяет наличие функции в выполняющем модуле. Декларация функций включает в себя спецификацию, и рекоммендуется спецификацию делать максимально детализированной. Это, в свою очередь, облегчит статическую проверку кода с помощю dialyzer-а.
В имплементирующих поведение модулях, а они в примере почти идентичны, кроме функции default, видно что декларация функций повторяет те, что находятся в определении поведения. В реальности декларация может быть любым сабсетом оригинальной функции, если конкретная реализация не создает все возможные сценарии.
Более интересен случай это создание цепочки поведений, когда сам модуль поведения в свою очередь выполняет другое поведение сам. Пример можно посмотреть в часности в стандартной библиотеке Erlang, когда поведение supervisor в свою очередь выполняет поведение gen_server. В этом случае код делится на две части — первая выполняет свои контрактные обязательства, вторая предоставляет служебные функции для других модулей по новому контракту.
В функциональном коде далеко не всегда есть необходимость в определении новых поведений. И критерий необходимости будет прост — два и больше модуля имеющие одинаковую семантику или роль, требуют определения интерфейса и унификации. Затраченое время облегчит и тестирование и будущее расширение кода. Потому-что если есть 2-3 модуля с одной ролью, высок шанс появления еще нескольких.