Добрый день.
Пара слов о себе сначала. Я пишу на 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 модуля с одной ролью, высок шанс появления еще нескольких.