Spec-Zone.ru › Elixir 1.17

Источник Антипаттерны, связанные с процессами

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

Организация кода по процессам

Проблема

Данный антипаттерн относится к коду, необоснованно организованному по процессам. Сам процесс не является антипаттерном, но он должен использоваться только для моделирования свойств выполнения (например, конкурентности, доступа к общим ресурсам, изоляции ошибок и т. д.). Когда вы используете процесс для организации кода, это может создавать узкие места в системе.

Пример

Пример этого антипаттерна, как показано ниже, — модуль, реализующий арифметические операции (например, add и subtract) с помощью процесса GenServer. Если количество вызовов этого единственного процесса увеличивается, такая организация кода может ухудшить производительность системы, превратившись в узкое место.

defmodule Calculator do
  @moduledoc """
  Calculator that performs basic arithmetic operations.

  This code is unnecessarily organized in a GenServer process.
  """

  use GenServer

  def add(a, b, pid) do
    GenServer.call(pid, {:add, a, b})
  end

  def subtract(a, b, pid) do
    GenServer.call(pid, {:subtract, a, b})
  end

  @impl GenServer
  def init(init_arg) do
    {:ok, init_arg}
  end

  @impl GenServer
  def handle_call({:add, a, b}, _from, state) do
    {:reply, a + b, state}
  end

  def handle_call({:subtract, a, b}, _from, state) do
    {:reply, a - b, state}
  end
end
iex> {:ok, pid} = GenServer.start_link(Calculator, :init)
{:ok, #PID<0.132.0>}
iex> Calculator.add(1, 5, pid)
6
iex> Calculator.subtract(2, 3, pid)
-1

Переработка

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

defmodule Calculator do
  def add(a, b) do
    a + b
  end

  def subtract(a, b) do
    a - b
  end
end
iex> Calculator.add(1, 5)
6
iex> Calculator.subtract(2, 3)
-1

Разбросанные интерфейсы процессов

Проблема

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

Пример

Следующий код призван проиллюстрировать этот антипаттерн. Ответственность за взаимодействие напрямую с Agent распределена по четырём разным модулям (A, B, C, и D).

defmodule A do
  def update(process) do
    # Some other code...
    Agent.update(process, fn _list -> 123 end)
  end
end
defmodule B do
  def update(process) do
    # Some other code...
    Agent.update(process, fn content -> %{a: content} end)
  end
end
defmodule C do
  def update(process) do
    # Some other code...
    Agent.update(process, fn content -> [:atom_value | content] end)
  end
end
defmodule D do
  def get(process) do
    # Some other code...
    Agent.get(process, fn content -> content end)
  end
end

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

# start an agent with initial state of an empty list
iex> {:ok, agent} = Agent.start_link(fn -> [] end)
{:ok, #PID<0.135.0>}

# many data formats (for example, List, Map, Integer, Atom) are
# combined through direct access spread across the entire system
iex> A.update(agent)
iex> B.update(agent)
iex> C.update(agent)

# state of shared information
iex> D.get(agent)
[:atom_value, %{a: 123}]

Для GenServer и других поведений этот антипаттерн проявится при разбросанном использовании вызовов GenServer.call/3 и GenServer.cast/2 по многим модулям вместо инкапсуляции всего взаимодействия с GenServer в одном месте.

Переработка

Вместо распределения прямого доступа к абстракции процесса, такой как Agent, по многим частям кода, лучше переработать этот код, централизовав ответственность за взаимодействие с процессом в одном модуле. Это улучшает поддерживаемость, удаляя дублирующийся код; также это позволяет ограничить принятый формат общих данных, уменьшая вероятность ошибок. Как показано ниже, модуль Foo.Bucket централизует ответственность за взаимодействие с Agent. Любое другое место в коде, которому нужен доступ к общим данным, должно теперь делегировать это действие модулю Foo.Bucket. Кроме того, Foo.Bucket теперь позволяет обмениваться данными только в формате Map.

defmodule Foo.Bucket do
  use Agent

  def start_link(_opts) do
    Agent.start_link(fn -> %{} end)
  end

  def get(bucket, key) do
    Agent.get(bucket, &Map.get(&1, key))
  end

  def put(bucket, key, value) do
    Agent.update(bucket, &Map.put(&1, key, value))
  end
end

Ниже приведены примеры делегирования доступа к общим данным (предоставляемым Agent) модулю Foo.Bucket.

# start an agent through `Foo.Bucket`
iex> {:ok, bucket} = Foo.Bucket.start_link(%{})
{:ok, #PID<0.114.0>}

# add shared values to the keys `milk` and `beer`
iex> Foo.Bucket.put(bucket, "milk", 3)
iex> Foo.Bucket.put(bucket, "beer", 7)

# access shared data of specific keys
iex> Foo.Bucket.get(bucket, "beer")
7
iex> Foo.Bucket.get(bucket, "milk")
3

Дополнительные замечания

Этот антипаттерн ранее был известен как зависимость от агента.

Передача ненужных данных

Проблема

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

Это более очевидно при использовании send/2, GenServer.call/3 или начальных данных в GenServer.start_link/3. Отметим, что это также происходит при использовании spawn/1, Task.async/1, Task.async_stream/3 и т. д. Здесь это более скрыто, так как анонимная функция, передаваемая этим функциям, захватывает переменные, на которые она ссылается, и все захваченные переменные будут скопированы. Таким образом, вы можете случайно передать процессу намного больше данных, чем вам действительно нужно.

Пример

Представьте, что вы реализуете простую отчётность по IP-адресам, которые делали запросы к вашему приложению. Вы хотите сделать это асинхронно и не блокировать обработку, поэтому вы решаете использовать spawn/1. Может показаться хорошей идеей передать всю информацию о соединении, потому что нам могут понадобиться дополнительные данные позже. Однако передача соединения приводит к копированию большого объёма ненужных данных, таких как тело запроса, параметры и т. д.

# log_request_ip send the ip to some external service
spawn(fn -> log_request_ip(conn) end)

Эта проблема также возникает при доступе только к соответствующим частям:

spawn(fn -> log_request_ip(conn.remote_ip) end)

Это всё равно скопирует весь conn, потому что переменная conn захватывается внутри функции, запускаемой с помощью spawn. Затем функция извлекает поле remote_ip, но только после копирования всего conn.

send/2 и API GenServer также полагаются на передачу сообщений. В приведённом ниже примере conn снова копируется в базовый GenServer:

GenServer.cast(pid, {:report_ip_address, conn})

Переработка

Этот антипаттерн имеет много потенциальных решений:

  • Ограничьте передаваемые данные абсолютным минимумом, вместо передачи всей структуры. Например, не передавайте всю структуру conn если вам нужны только несколько полей.

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

  • Некоторые абстракции, такие как :persistent_term, позволяют обмениваться данными между процессами, если такие данные изменяются редко.

В нашем случае ограничение входных данных — разумная стратегия. Если нам прямо сейчас нужен только IP-адрес, то давайте работать только с ним и убедимся, что мы передаём только IP-адрес в замыкание, как показано ниже:

ip_address = conn.remote_ip
spawn(fn -> log_request_ip(ip_address) end)

Или в случае GenServer:

GenServer.cast(pid, {:report_ip_address, conn.remote_ip})

Незавизированные процессы

Проблема

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

Пример

Следующий пример кода иллюстрирует библиотеку, ответственного за поддержание числового Counter через процесс GenServer вне дерева управления. Клиент (один процесс на каждый счётчик) может создать несколько счётчиков одновременно, что делает эти незавизированные процессы трудно управляемыми. Это может вызвать проблемы с инициализацией, перезапуском и завершением работы системы.

defmodule Counter do
  @moduledoc """
  Global counter implemented through a GenServer process.
  """

  use GenServer

  @doc "Starts a counter process."
  def start_link(opts \\ []) do
    initial_value = Keyword.get(opts, :initial_value, 0)
    name = Keyword.get(opts, :name, __MODULE__)
    GenServer.start(__MODULE__, initial_value, name: name)
  end

  @doc "Gets the current value of the given counter."
  def get(pid_name \\ __MODULE__) do
    GenServer.call(pid_name, :get)
  end

  @doc "Bumps the value of the given counter."
  def bump(pid_name \\ __MODULE__, value) do
    GenServer.call(pid_name, {:bump, value})
  end

  @impl true
  def init(counter) do
    {:ok, counter}
  end

  @impl true
  def handle_call(:get, _from, counter) do
    {:reply, counter, counter}
  end

  def handle_call({:bump, value}, _from, counter) do
    {:reply, counter, counter + value}
  end
end
iex> Counter.start_link()
{:ok, #PID<0.115.0>}
iex> Counter.get()
0
iex> Counter.start_link(initial_value: 15, name: :other_counter)
{:ok, #PID<0.120.0>}
iex> Counter.get(:other_counter)
15
iex> Counter.bump(:other_counter, -3)
12
iex> Counter.bump(Counter, 7)
7

Переработка

Чтобы гарантировать, что клиенты библиотеки имеют полный контроль над своими системами, независимо от количества используемых процессов и срока жизни каждого из них, все процессы должны запускаться внутри дерева управления. Как показано ниже, этот код использует Supervisor в качестве дерева управления. При запуске этого приложения Elixir также запускаются два счётчика (Counter и :other_counter ) как дочерние процессы Supervisor с именем App.Supervisor. Один инициализируется значением 0, другой — значением 15. С помощью этого дерева управления можно управлять жизненным циклом всех дочерних процессов (останавливать или перезапускать каждый из них), улучшая видимость всего приложения.

defmodule SupervisedProcess.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # With the default values for counter and name
      Counter,
      # With custom values for counter, name, and a custom ID
      Supervisor.child_spec(
        {Counter, name: :other_counter, initial_value: 15},
        id: :other_counter
      )
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: App.Supervisor)
  end
end
iex> Supervisor.count_children(App.Supervisor)
%{active: 2, specs: 2, supervisors: 0, workers: 2}
iex> Counter.get(Counter)
0
iex> Counter.get(:other_counter)
15
iex> Counter.bump(Counter, 7)
7
iex> Supervisor.terminate_child(App.Supervisor, Counter)
iex> Supervisor.count_children(App.Supervisor) # Only one active child
%{active: 1, specs: 2, supervisors: 0, workers: 2}
iex> Counter.get(Counter) # The process was terminated
** (EXIT) no process: the process is not alive...
iex> Supervisor.restart_child(App.Supervisor, Counter)
iex> Counter.get(Counter) # After the restart, this process can be used again
0
← Предыдущая страница Антипаттерны, связанные с проектированием
Следующая страница → Антипаттерны метапрограммирования

Скачать версию ePub

Создано с помощью ExDoc (v0.34.1) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/process-anti-patterns.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API