Процессно-ориентированное программирование в Elixir и OTP

Людям нравится классифицировать языки программирования на парадигмы. Существуют объектно-ориентированные (ООП) языки, императивные языки, функциональные языки и т. д. Это может быть полезно при определении того, какие языки решают подобные проблемы, и какие типы проблем должен решать язык.

В каждом случае парадигма обычно имеет один «главный» фокус и технику, которая является движущей силой для этого семейства языков:

  • В языках OOП это класс или объект как способ инкапсулировать состояние (данные) с помощью манипуляции с этим состоянием (методами).
  • В функциональных языках это может быть манипуляция самих функций или неизменяемых данных, переданных от функции к функции.

Хотя ElixirErlang до него) часто классифицируются как функциональные языки, потому что они демонстрируют неизменные данные, общие для функциональных языков, я бы представил, что они представляют собой отдельную парадигму от многих функциональных языков. Они существуют и принимаются из-за существования OTP, и поэтому я буду классифицировать их как ориентированные на процесс языки.

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

Что такое процессно-ориентированное программирование?

Начнем с определения: Процессно-ориентированное программирование — это парадигма, основанная на передаче последовательных процессов, первоначально из статьи Тони Хоара в 1977 году. Это также широко называют актерской моделью параллелизма. Другие языки, имеющие отношение к этой оригинальной работе, включают Occam, Limbo и Go. В официальном документе речь идет только об синхронной связи; большинство моделей актеров (включая OTP ) также используют асинхронную связь. Всегда можно строить синхронную связь поверх асинхронной связи, а OTP поддерживает обе формы.

В этой истории OTP создала систему отказоустойчивых вычислений путем передачи последовательных процессов. Отказоустойчивые средства исходят из подхода «пусть это неудачно» с надежным восстановлением ошибок в виде супервизоров и использования распределенной обработки, разрешенной моделью актера. «Разрешить потерпеть неудачу» можно противопоставить «предотвратить его от сбоев», поскольку первый гораздо проще разместить и был доказан в OTP, чтобы быть гораздо более надежным, чем последний. Причина в том, что усилия по программированию, необходимые для предотвращения сбоев (как показано в проверенной модели исключения Java), гораздо более сложны и требовательны.

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

Объектно-ориентированное и технологическое программирование

В объектно-ориентированном программировании главной задачей является статическая структура данных и функций. Какие методы необходимы для управления закрытыми данными и какими должны быть связи между объектами или классами. Таким образом, диаграмма классов UML является ярким примером этого фокуса, как показано на рисунке 1.

Можно отметить, что общая критика объектно-ориентированного программирования заключается в отсутствии видимого потока управления. Поскольку системы состоят из большого количества классов / объектов, определенных отдельно, для менее опытного человека может быть сложно визуализировать поток управления системой. Это особенно справедливо для систем с большим количеством наследования, которые используют абстрактные интерфейсы или не имеют сильной типизации. В большинстве случаев разработчику очень важно помнить значительное количества системной структуры (какие классы имеют какие методы и которые используются каким образом).

Сила подхода объектно-ориентированного развития заключается в том, что система может быть расширена для поддержки новых типов объектов с ограниченным воздействием на существующий код, если новые типы объектов соответствуют ожиданиям существующего кода.

Функциональное и технологическое программирование

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

Например, Scala — это функциональный язык, созданный на виртуальной машине Java. Хотя он может обращаться к средствам Java для связи, это не является неотъемлемой частью языка. Хотя это общий язык, используемый в программировании Spark, он снова является библиотекой, используемой в сочетании с языком.

Силой функциональной парадигмы является способность визуализировать управляющий поток системы с учетом функции верхнего уровня. В потоке управления явно указано, что каждая функция вызывает другие функции и передает все данные от одного к другому. В функциональной парадигме нет побочных эффектов, что облегчает определение проблемы. Задача с чистыми функциональными системами заключается в том, что «побочные эффекты» должны иметь постоянное состояние. В хорошо сконструированных системах сохраняющееся состояние обрабатывается на верхнем уровне потока управления, что позволяет большей части системы быть свободным от побочных эффектов.

Эликсир / ОТП и технологическое программирование

В Elixir / Erlang и OTP примитивы связи являются частью виртуальной машины, которая выполняет язык. Возможность взаимодействия между процессами и между машинами встроена и центральна для языковой системы. Это подчеркивает важность коммуникации в этой парадигме и в этих языковых системах.

Хотя язык эликсира преимущественно функционирует с точки зрения логики, выраженной на языке, его использование ориентировано на процесс.

Что значит быть ориентированным на процесс?

Чтобы быть ориентированным на процесс, как определено в этой статье, необходимо сначала разработать систему в виде того, какие процессы существуют и как они обмениваются данными. Одним из основных вопросов является то, какие процессы являются статическими и которые являются динамическими, которые порождаются по запросу запросам, которые служат долговременной цели, которые содержат разделяемое состояние или часть общего состояния системы и какие функции система по своей сути является параллельной. Подобно тому, как ООП имеет типы объектов, а функционал имеет типы функций, процесс-ориентированное программирование имеет типы процессов.

Таким образом, процесс-ориентированный дизайн — это идентификация набора типов процессов, необходимых для решения проблемы или удовлетворения потребности .

Аспект времени быстро входит в разработку и требования. Каков жизненный цикл системы? Какие особые потребности являются случайными и постоянными? Где находится нагрузка в системе и какова ожидаемая скорость и объем? Только после того, как понятны эти типы соображений, процесс-ориентированный дизайн начинает определять функцию каждого процесса или логику, которая должна быть выполнена.

Тренировочные последствия

Следствием этой категоризации для обучения является то, что обучение должно начинаться не с синтаксиса языка или примеров «Hello World», а с системного мышления и централизованного проектирования.

Проблемы с кодированием являются вторичными по отношению к дизайну и распределению процессов, которые лучше всего рассматриваются на более высоком уровне, и включают в себя кросс-функциональное мышление о жизненном цикле, QA, DevOps и требованиях клиента к бизнесу. Любой учебный курс в Elixir или Erlang должен (и, как правило, вообще) включать OTP и должен иметь ориентацию процесса с самого начала, а не как «Теперь вы можете кодировать в стиле Elixir, так что давайте сделаем параллелизм».

Последствия усвоения

Примером для принятия является то, что язык и система лучше применяются к проблемам, которые требуют коммуникации и / или распределения вычислений. Проблемы, которые представляют собой единую рабочую нагрузку на одном компьютере, менее интересны в этом пространстве и могут быть лучше реализованы другим языком. Долговечные системы непрерывной обработки являются главной мишенью для этого языка, поскольку он имеет отказоустойчивость, встроенную с нуля.

Для документации и проектных работ может быть очень полезно использовать графическое обозначение (например, рисунок 1 для языков OO). Предложение для Elixir и ориентированное на процесс программирование с UML было бы диаграммой последовательности (пример на рисунке 2), чтобы показать временные отношения между процессами и определить, какие процессы задействованы в обслуживании запроса. Нет типа UML-диаграммы для записи жизненного цикла и структуры процесса, но он может быть представлен простой коробкой и стрелочной диаграммой для типов процессов и их отношений. Например, Рисунок 3:

Пример ориентации процесса

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

Начальный процесс проектирования и распределения

Мы можем изначально видеть, что выбор голосов каждым человеком — это поток трафика в систему со многих дискретных входов, не упорядоченный по времени и может иметь высокую нагрузку. Чтобы поддержать эту деятельность, мы хотели бы, чтобы большое количество процессов собирало эти данные и направляло их в более центральный процесс для табуляции. Эти процессы могут быть расположены вблизи населения в каждой стране, которое будет генерировать голоса и, таким образом, обеспечить низкую задержку. Они сохраняют локальные результаты, немедленно регистрируют свои входы и отправляют их для табулирования партиями, чтобы уменьшить пропускную способность и накладные расходы.

Первоначально мы можем видеть, что должны быть процессы, которые отслеживают голоса в каждой юрисдикции, в которой должны быть представлены результаты.Предположим для этого примера, что нам нужно отслеживать результаты для каждой страны и внутри каждой страны по провинции / штату. Чтобы поддержать эту деятельность, мы хотели бы, по крайней мере, один процесс для каждой страны, выполняющей расчет, и сохранение текущих итогов, а также другой набор для каждого штата / провинции в каждой стране. Это предполагает, что мы должны иметь возможность отвечать на итоговые данные для страны и штата / провинции в режиме реального времени или с низкой задержкой. Если результаты могут быть получены из системы баз данных, мы можем выбрать другое распределение процессов, когда итоговые значения обновляются с помощью переходных процессов. Преимущество использования выделенных процессов для этих вычислений состоит в том, что результаты происходят со скоростью памяти и могут быть получены с низкой задержкой.

Наконец, мы видим, что много и много людей будут просматривать результаты. Эти процессы могут быть разделены разными способами. Мы можем захотеть распределить нагрузку, поставив процессы в каждой стране, ответственные за результаты этой страны. Процессы могут кэшировать результаты процессов вычислений, чтобы уменьшить нагрузку запросов на процессы вычислений, и / или процессы вычислений могут периодически подталкивать результаты к соответствующим процессам результатов, когда результаты изменяются на значительную величину или на процесс вычисления становится бездействующим, что указывает на замедленную скорость изменения.

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

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

Использование отдельных процессов для получения голосов позволяет получать каждое голосование независимо от любого другого голосования, регистрироваться при получении и переноситься на следующий набор процессов, значительно уменьшая нагрузку на эти системы. Для системы, которая потребляет большой объем данных, сокращение объема данных с использованием слоев процессов является общей и полезной моделью.

Выполняя вычисление в изолированном наборе процессов, мы можем управлять нагрузкой на эти процессы и обеспечивать их стабильность и требования к ресурсам.

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

Дополнительные требования

Теперь давайте добавим некоторые усложняющие требования. Предположим, что в каждой юрисдикции (стране или государстве) табуляция голосов может привести к пропорциональному результату, результату победителя или к результату, если не будет подано недостаточно голосов относительно населения этой юрисдикции. Каждая юрисдикция контролирует эти аспекты. С учетом этого изменения результаты стран не являются простой совокупностью результатов необработанного голосования, а представляют собой совокупность результатов состояния / провинции. Это изменяет распределение процесса от оригинала, чтобы потребовать, чтобы результаты процессов состояния / провинции обрабатывались в процессах страны. Если протокол, используемый между сбором голосов и состояниями / провинциями и провинциями в стране, одинаковый, то логику агрегации можно повторно использовать, но необходимы различные процессы, содержащие результаты, и их пути связи различны, как показано на рисунке 5.

Код

Чтобы завершить этот пример, мы рассмотрим реализацию примера в Elixir OTP. Для упрощения этого примера предполагается, что веб-сервер, такой как Phoenix, используется для обработки фактических веб-запросов, и эти веб-службы делают запросы к процессу, указанному выше. Это имеет преимущество, упрощая пример и сохраняя ориентацию на Elixir/OTP. В производственной системе наличие этих отдельных процессов имеет некоторые преимущества, а также разделяет проблемы, позволяет гибко развертывать, распределять нагрузку и сокращать время ожидания. Полный исходный код с тестами можно найти на странице https://github.com/technomage/voting . Источник сокращен в этом сообщении для удобства чтения. Каждый процесс ниже вписывается в дерево контроля OTP, чтобы гарантировать перезапуск процессов при сбое. Дополнительную информацию об этом аспекте примера см. В источнике.

Регистратор голосов

Этот процесс получает голоса, регистрирует их в постоянном хранилище и разбивает результаты на агрегаторы. Модуль VoteRecoder использует Task.Supervisor для управления краткосрочными задачами для записи каждого голоса.

defmodule Voting.VoteRecorder do
  @moduledoc """
  This module receives votes and sends them to the proper
  aggregator. This module uses supervised tasks to ensure
  that any failure is recovered from and the vote is not
  lost.
  """

  @doc """
  Start a task to track the submittal of a vote to an
  aggregator. This is a supervised task to ensure
  completion.
  """
  def cast_vote where, who do
    Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor,
      fn ->
        Voting.Aggregator.submit_vote where, who
      end)
    |> Task.await
  end
end

Агрегатор голосов

Этот процесс объединяет голоса в пределах юрисдикции, вычисляет результат для этой юрисдикции и пересылает резюме в следующий более высокий процесс (юрисдикция более высокого уровня или ведущий результата).

defmodule Voting.Aggregator do
  use GenStage
  ...

  @doc """
  Submit a single vote to an aggregator
  """
  def submit_vote id, candidate do
    pid = __MODULE__.via_tuple(id)
    :ok = GenStage.call pid, {:submit_vote, candidate}
  end

  @doc """
  Respond to requests
  """
  def handle_call {:submit_vote, candidate}, _from, state do
    n = state.votes[candidate] || 0
    state = %{state | votes: Map.put(state.votes, candidate, n+1)}
    {:reply, :ok, [%{state.id => state.votes}], state}
  end

  @doc """
  Handle events from subordinate aggregators
  """
  def handle_events events, _from, state do
    votes = Enum.reduce events, state.votes, fn e, votes ->
      Enum.reduce e, votes, fn {k,v}, votes ->
        Map.put(votes, k, v) # replace any entries for subordinates
      end
    end
    # Any jurisdiction specific policy would go here

    # Sum the votes by candidate for the published event
    merged = Enum.reduce votes, %{}, fn {j, jv}, votes ->
      # Each jourisdiction is summed for each candidate
      Enum.reduce jv, votes, fn {candidate, tot}, votes ->
        Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}"
        n = votes[candidate] || 0
        Map.put(votes, candidate, n + tot)
      end
    end
    # Return the published event and the state which retains
    # Votes by jourisdiction
    {:noreply, [%{state.id => merged}], %{state | votes: votes}}
  end
end

Результат

Этот процесс получает голоса от агрегатора и кэширует эти результаты для запросов на обслуживание для представления результатов.

defmodule Voting.ResultPresenter do
  use GenStage
  …

  @doc """
  Handle requests for results
  """
  def handle_call :get_votes, _from, state do
    {:reply, {:ok, state.votes}, [], state}
  end

  @doc """
  Obtain the results from this presenter
  """
  def get_votes id do
    pid = Voting.ResultPresenter.via_tuple(id)
    {:ok, votes} = GenStage.call pid, :get_votes
    votes
  end

  @doc """
  Receive votes from aggregator
  """
  def handle_events events, _from, state do
    Logger.debug "@@@@ Presenter received: #{inspect events}"
    votes = Enum.reduce events, state.votes, fn v, votes ->
      Enum.reduce v, votes, fn {k,v}, votes ->
        Map.put(votes, k, v)
      end
    end
    {:noreply, [], %{state | votes: votes}}
  end
end

На вынос

Этот пост исследовал Elixir / OTP из своего потенциала как ориентированного на процесс языка, сравнивал его с объектно-ориентированными и функциональными парадигмами и рассматривал последствия этого для обучения и принятия.

Сообщение также включает короткий пример применения этой ориентации к проблеме выборки. Если вы хотите просмотреть весь код, вот ссылка на наш пример на GitHub снова, так что вам не нужно прокручивать назад, ища его.

Ключевым выводом является просмотр систем как совокупности коммуникационных процессов. Сначала планируйте систему с точки зрения проектирования процесса, а далее приступайте к логической кодировке.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *