Spec-Zone.ru › Elixir 1.18

Исходный код Антипаттерны, связанные с кодом

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

Чрезмерное использование комментариев

Проблема

Чрезмерное использование комментариев или комментирование очевидного кода может сделать код менее читаемым.

Пример

# Returns the Unix timestamp of 5 minutes from the current time
defp unix_five_min_from_now do
  # Get the current time
  now = DateTime.utc_now()

  # Convert it to a Unix timestamp
  unix_now = DateTime.to_unix(now, :second)

  # Add five minutes in seconds
  unix_now + (60 * 5)
end

Переработка

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

Вы можете переработать код так:

@five_min_in_seconds 60 * 5

defp unix_five_min_from_now do
  now = DateTime.utc_now()
  unix_now = DateTime.to_unix(now, :second)
  unix_now + @five_min_in_seconds
end

Мы удалили ненужные комментарии. Мы также добавили атрибут модуля @five_min_in_seconds, который дополнительно назначает имя «магическому» числу 60 * 5, что делает код более ясным и выразительным.

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

Elixir четко разграничивает документацию и комментарии к коду. Язык имеет встроенную первоклассную поддержку документации через @doc, @moduledoc, и многое другое. См. руководство "Написание документации" для получения дополнительной информации.

Сложные else-блоки в with

Проблема

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

Пример

Пример этого антипаттерна, показанный ниже, — функция open_decoded_file/1, которая считывает содержимое строки, закодированной в Base64, из файла и возвращает декодированную двоичную строку. Эта функция использует with-выражение, которое должно обрабатывать две возможные ошибки, все из которых сосредоточены в одном сложном else-блоке.

def open_decoded_file(path) do
  with {:ok, encoded} <- File.read(path),
       {:ok, decoded} <- Base.decode64(encoded) do
    {:ok, String.trim(decoded)}
  else
    {:error, _} -> {:error, :badfile}
    :error -> {:error, :badencoding}
  end
end

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

Переработка

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

def open_decoded_file(path) do
  with {:ok, encoded} <- file_read(path),
       {:ok, decoded} <- base_decode64(encoded) do
    {:ok, String.trim(decoded)}
  end
end

defp file_read(path) do
  case File.read(path) do
    {:ok, contents} -> {:ok, contents}
    {:error, _} -> {:error, :badfile}
  end
end

defp base_decode64(contents) do
  case Base.decode64(contents) do
    {:ok, decoded} -> {:ok, decoded}
    :error -> {:error, :badencoding}
  end
end

Сложные извлечения в секциях

Проблема

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

Пример

Функция с несколькими секциями drive/1 извлекает поля структуры %User{} для использования в выражении секции (age) и для использования в теле функции (name):

def drive(%User{name: name, age: age}) when age >= 18 do
  "#{name} can drive"
end

def drive(%User{name: name, age: age}) when age < 18 do
  "#{name} cannot drive"
end

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

Переработка

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

def drive(%User{age: age} = user) when age >= 18 do
  %User{name: name} = user
  "#{name} can drive"
end

def drive(%User{age: age} = user) when age < 18 do
  %User{name: name} = user
  "#{name} cannot drive"
end

Динамическое создание атомов

Проблема

Атом — это базовый тип Elixir, значение которого является его собственным именем. Атомы часто полезны для идентификации ресурсов или выражения состояния или результата операции. Динамическое создание атомов само по себе не является антипаттерном. Однако атомы не собираются сборщиком мусора виртуальной машиной Erlang, поэтому значения этого типа хранятся в памяти в течение всего времени выполнения программы. Виртуальная машина Erlang по умолчанию ограничивает количество атомов, которые могут существовать в приложении, до 1 048 576, чего более чем достаточно для всех атомов, определенных в программе, но попытки служат ранним ограничением для приложений, которые «утечка атомов» через динамическое создание.

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

Пример

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

defmodule MyRequestHandler do
  def parse(%{"status" => status, "message" => message} = _payload) do
    %{status: String.to_atom(status), message: message}
  end
end
iex> MyRequestHandler.parse(%{"status" => "ok", "message" => "all good"})
%{status: :ok, message: "all good"}

При использовании функции String.to_atom/1 для динамического создания атома, она потенциально получает доступ к созданию произвольных атомов в нашей системе, что приводит к потере контроля над соблюдением ограничений, установленных BEAM. Эта проблема может быть использована кем-то для создания достаточного количества атомов, чтобы остановить работу системы.

Переработка

Чтобы устранить этот антипаттерн, разработчики должны либо выполнить явное преобразование, сопоставив строки с атомами, или заменить использование String.to_atom/1 на String.to_existing_atom/1. Явное преобразование может быть выполнено следующим образом:

defmodule MyRequestHandler do
  def parse(%{"status" => status, "message" => message} = _payload) do
    %{status: convert_status(status), message: message}
  end

  defp convert_status("ok"), do: :ok
  defp convert_status("error"), do: :error
  defp convert_status("redirect"), do: :redirect
end
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
** (FunctionClauseError) no function clause matching in MyRequestHandler.convert_status/1

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

Альтернативой является использование String.to_existing_atom/1, которое преобразует строку в атом только в том случае, если атом уже существует в системе:

defmodule MyRequestHandler do
  def parse(%{"status" => status, "message" => message} = _payload) do
    %{status: String.to_existing_atom(status), message: message}
  end
end
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not an already existing atom

В таких случаях передача неизвестного статуса вызовет ошибку, если статус не определён как атом в системе. Однако, если status может быть либо :ok, либо :error, либо :redirect, как вы можете гарантировать существование этих атомов? Вам нужно убедиться, что эти атомы существуют где-то в том же модуле, где вызывается String.to_existing_atom/1. Например, если у вас такой код:

defmodule MyRequestHandler do
  def parse(%{"status" => status, "message" => message} = _payload) do
    %{status: String.to_existing_atom(status), message: message}
  end

  def handle(%{status: status}) do
    case status do
      :ok -> ...
      :error -> ...
      :redirect -> ...
    end
  end
end

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

def valid_statuses do
  [:ok, :error, :redirect]
end

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

Длинный список параметров

Проблема

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

Пример

В следующем примере функция loan/6 принимает слишком много аргументов, что делает её интерфейс запутанным и потенциально приводит к ошибкам при вызовах этой функции.

defmodule Library do
  # Too many parameters that can be grouped!
  def loan(user_name, email, password, user_alias, book_title, book_ed) do
    ...
  end
end

Переработка

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

В этом конкретном примере аргументы для loan/6 можно сгруппировать в две различные карты, тем самым уменьшив их арность до loan/2:

defmodule Library do
  def loan(%{name: name, email: email, password: password, alias: alias} = user, %{title: title, ed: ed} = book) do
    ...
  end
end

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

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

Нарушение пространства имён

Проблема

Этот антипаттерн проявляется, когда автор пакета или библиотеки определяет модули вне своего «пространства имён». Библиотека должна использовать своё имя в качестве «префикса» для всех своих модулей. Например, пакет под названием :my_lib должен определять все свои модули в пространстве имён MyLib, таком как MyLib.User, MyLib.SubModule, MyLib.Application, и сам MyLib.

Это важно, так как виртуальная машина Erlang может загрузить только один экземпляр модуля за раз. Таким образом, если несколько библиотек определяют один и тот же модуль, они несовместимы друг с другом из-за этого ограничения. Использование имени библиотеки в качестве префикса позволяет избежать конфликтов имён модулей из-за уникального префикса.

Пример

Эта проблема часто проявляется при написании расширения другой библиотеки. Например, представьте, что вы пишете пакет, добавляющий аутентификацию к Plug, названный :plug_auth. Необходимо избегать определения модулей внутри пространства имён Plug.

defmodule Plug.Auth do
  # ...
end

Даже если Plug в настоящее время не определяет модуль Plug.Auth, он может добавить такой модуль в будущем, что в конечном итоге приведёт к конфликту с определением plug_auth.

Рефакторинг

Учитывая, что пакет называется :plug_auth, необходимо определять модули внутри пространства имён PlugAuth:

defmodule PlugAuth do
  # ...
end

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

Существует несколько известных исключений из этого антипаттерна:

  • Реализации протоколов по своему дизайну определяются в пространстве имён протокола.

  • В некоторых случаях владелец пространства имён может разрешить исключения из этого правила. Например, в самом Elixir вы определяете пользовательские задачи Mix, размещая их в пространстве имён Mix.Tasks, например Mix.Tasks.PlugAuth.

  • Если вы являетесь разработчиком и plug и plug_auth, то вы можете разрешить plug_auth определять модули с пространством имён Plug, например Plug.Auth. Однако вы несете ответственность за предотвращение или управление возможными будущими конфликтами.

Доступ к картам без утверждений

Проблема

В Elixir можно получить доступ к значениям из Map, которые представляют собой структуры данных «ключ-значение», статически или динамически.

Если ожидается, что ключ существует в карте, необходимо использовать обозначение map.key, что делает для разработчиков (и компилятора) ясным, что ключ должен существовать. Если ключ не существует, генерируется исключение (а в некоторых случаях также предупреждения компилятора). Это также известно как статическое обозначение, так как ключ известен на момент написания кода.

Если ключ является необязательным, необходимо использовать обозначение map[:key] вместо него. Таким образом, если указанный ключ не существует, возвращается nil. Это динамическое обозначение, так как оно также поддерживает динамический доступ к ключу, такой как map[some_var].

Использование map[:key] для доступа к ключу, который всегда существует в карте, делает код менее понятным для разработчиков и компилятора, так как им теперь нужно исходить из предположения, что ключ может отсутствовать. Это несоответствие также может затруднить отслеживание определённых ошибок. Если ключ неожиданно отсутствует, значение nil будет распространяться по системе вместо генерации исключения при доступе к карте.

Пример

Функция plot/1 пытается отобразить графическое представление положения точки в декартовой плоскости. Эта функция получает параметр типа Map с атрибутами точки, которые могут быть точкой 2D или 3D декартовой системы координат. Эта функция использует динамический доступ для получения значений для ключей карты:

defmodule Graphics do
  def plot(point) do
    # Some other code...
    {point[:x], point[:y], point[:z]}
  end
end
iex> point_2d = %{x: 2, y: 3}
%{x: 2, y: 3}
iex> point_3d = %{x: 5, y: 6, z: 7}
%{x: 5, y: 6, z: 7}
iex> Graphics.plot(point_2d)
{2, 3, nil}
iex> Graphics.plot(point_3d)
{5, 6, 7}

Учитывая, что мы хотим отображать как 2D, так и 3D точки, ожидается поведение, описанное выше. Но что произойдёт, если мы забудем передать точку с ключом :x или :y?

iex> bad_point = %{y: 3, z: 4}
%{y: 3, z: 4}
iex> Graphics.plot(bad_point)
{nil, 3, 4}

Поведение, описанное выше, непредсказуемо, потому что наша функция не должна работать с точками без ключа :x. Это приводит к скрытым ошибкам, так как мы можем передать nil в другую функцию вместо того, чтобы генерировать исключение на ранней стадии.

Рефакторинг

Для устранения этого антипаттерна необходимо использовать синтаксис динамического map[:key] и статическое обозначение map.key в соответствии с нашими требованиями. Мы ожидаем, что :x и :y всегда будут существовать, но не :z.

Следующий код демонстрирует рефакторинг функции plot/1, устраняя этот антипаттерн:

defmodule Graphics do
  def plot(point) do
    # Some other code...
    {point.x, point.y, point[:z]}
  end
end
iex> Graphics.plot(point_2d)
{2, 3, nil}
iex> Graphics.plot(bad_point)
** (KeyError) key :x not found in: %{y: 3, z: 4}
  graphic.ex:4: Graphics.plot/1

В целом, использование map.key и map[:key] кодирует важную информацию о вашей структуре данных, позволяя разработчикам быть ясными в своих намерениях. См. документацию модулей Map и Access для получения дополнительной информации и примеров.

Альтернативный способ исправить этот антипаттерн — использование сопоставления с образцом, определённого явных правил для 2D и 3D точек:

defmodule Graphics do
  # 3d
  def plot(%{x: x, y: y, z: z}) do
    # Some other code...
    {x, y, z}
  end

  # 2d
  def plot(%{x: x, y: y}) do
    # Some other code...
    {x, y}
  end
end

Сопоставление с образцом особенно полезно при сопоставлении сразу с несколькими ключами и самими значениями.

Ещё один вариант — использование структур. По умолчанию структуры поддерживают только статический доступ к своим полям. В таких сценариях вы можете рассмотреть возможность определения структур для 2D и 3D точек:

defmodule Point2D do
  @enforce_keys [:x, :y]
  defstruct [x: nil, y: nil]
end

Как правило, структуры полезны при совместном использовании структур данных между модулями, но при этом добавляется зависимость от компиляции между этими модулями. Если модуль A использует структуру, определённую в модуле B, A необходимо перекомпилировать, если поля в структуре B изменятся.

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

Этот антипаттерн ранее известен как Доступ к несуществующим полям карты/структуры.

Неутверждающее сопоставление с образцом

Проблема

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

Пример

Функция get_value/2 пытается извлечь значение из определённого ключа строки запроса URL. Поскольку она не реализована с использованием сопоставления с образцом, get_value/2 всегда возвращает значение, независимо от формата строки запроса URL, переданной в качестве параметра вызова. Иногда возвращаемое значение будет допустимым. Однако, если используется строка запроса URL с неожиданным форматом в вызове, get_value/2 будет извлекать из неё неверные значения:

defmodule Extract do
  def get_value(string, desired_key) do
    parts = String.split(string, "&")

    Enum.find_value(parts, fn pair ->
      key_value = String.split(pair, "=")
      Enum.at(key_value, 0) == desired_key && Enum.at(key_value, 1)
    end)
  end
end
# URL query string with the planned format - OK!
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "lab")
"ASERG"
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "university")
"UFMG"
# Unplanned URL query string format - Unplanned value extraction!
iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university")
"institution"   # <= why not "institution=UFMG"? or only "UFMG"?

Рефакторинг

Для устранения этого антипаттерна get_value/2 можно переписать с использованием сопоставления с образцом. Таким образом, если используется неожиданный формат строки запроса URL, функция аварийно завершается вместо возвращения недействительного значения. Это поведение, показанное ниже, позволяет клиентам решать, как обрабатывать эти ошибки, и не даёт ложного представления о том, что код работает правильно, когда извлекаются неожиданные значения:

defmodule Extract do
  def get_value(string, desired_key) do
    parts = String.split(string, "&")

    Enum.find_value(parts, fn pair ->
      [key, value] = String.split(pair, "=") # <= pattern matching
      key == desired_key && value
    end)
  end
end
# URL query string with the planned format - OK!
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "name")
"Lucas"
# Unplanned URL query string format - Crash explaining the problem to the client!
iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university")
** (MatchError) no match of right hand side value: ["university", "institution", "UFMG"]
  extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair
iex> Extract.get_value("name=Lucas&university&lab=ASERG", "university")
** (MatchError) no match of right hand side value: ["university"]
  extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair

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

case/2 — ещё один важный конструкт в Elixir, который помогает нам писать утверждающий код, сопоставляя его с конкретными образцами. Например, если функция возвращает {:ok, ...} или {:error, ...}, предпочитайте явно сопоставлять оба шаблона:

case some_function(arg) do
  {:ok, value} -> # ...
  {:error, _} -> # ...
end

В частности, избегайте сопоставления только с _, как показано ниже:

case some_function(arg) do
  {:ok, value} -> # ...
  _ -> # ...
end

Сопоставление с _ менее ясно в плане намерений, и это может скрывать ошибки, если some_function/1 добавит новые значения возврата в будущем.

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

Этот антипаттерн ранее известен как Спекулятивные предположения.

Неутверждающая истинность

Проблема

Elixir предоставляет понятие истинности: nil и false считаются «ложными», а все остальные значения — «истинными». Многие конструкции языка, такие как &&/2, ||/2 и !/1, обрабатывают истинные и ложные значения. Использование этих операторов не является антипаттерном. Однако использование этих операторов, когда все операнды ожидаются как булевы, может быть антипаттерном.

Пример

Простейший случай проявления этого антипаттерна — в условных операторах, таких как:

if is_binary(name) && is_integer(age) do
  # ...
else
  # ...
end

Поскольку оба операнда &&/2 — булевы, код более общий, чем необходимо, и, потенциально, менее понятный.

Рефакторинг

Для устранения этого антипаттерна можно заменить &&/2, ||/2 и !/1 на and/2, or/2 и not/1 соответственно. Эти операторы предполагают, что, по крайней мере, первый аргумент — булев:

if is_binary(name) and is_integer(age) do
  # ...
else
  # ...
end

Этот приём может быть особенно важным при работе с кодом Erlang. Erlang не имеет понятия истинности. Он никогда не возвращает nil, вместо этого его функции могут возвращать :error или :undefined в тех местах, где разработчик Elixir вернул бы nil. Поэтому, чтобы избежать случайной интерпретации :undefined или :error как истинного значения, лучше использовать and/2, or/2 и not/1 исключительно при взаимодействии с API Erlang.

Структуры с 32 полями или более

Проблема

Структуры в Elixir реализуются как карты во время компиляции, у которых есть предварительно определённое количество полей. Когда структуры имеют 32 или более полей, их внутреннее представление в виртуальной машине Erlang меняется, что может привести к увеличению размера и большему потреблению памяти.

Пример

Любая структура с 32 или более полями будет проблематичной:

defmodule MyExample do
  defstruct [
    :field1,
    :field2,
    ...,
    :field35
  ]
end

В виртуальной машине Erlang существует два внутренних представления для карт: плоская карта и карта хешей. Плоская карта представляется внутри как две кортежи: один кортеж, содержащий ключи, и другой кортеж, содержащий значения. При обновлении плоской карты ключи кортежа разделяются, что сокращает количество используемой памяти при обновлении. У карты хешей более сложная структура, которая эффективна при большом количестве ключей, но она не разделяет пространство ключей.

Карты до 32 ключей представлены как плоские карты. Все остальные — это хэш-карты. Структуры являются картами (с метаданными, называемыми __struct__) и поэтому любая структура с менее чем 32 полями представлена как плоская карта. Это позволяет оптимизировать несколько операций со структурами, так как мы никогда не добавляем или не удаляем поля из структур, мы просто обновляем их.

Кроме того, структуры с одинаковым именем, «инициализированные» в одном модуле, будут использовать одни и те же «кортежные ключи» во время компиляции, при условии, что они имеют менее чем 32 поля. Например, в следующем коде:

defmodule Example do
  def users do
    [%User{name: "John"}, %User{name: "Meg"}, ...]
  end
end

Все пользовательские структуры будут указывать на одни и те же кортежные ключи во время компиляции, что также снижает затраты памяти на инициализацию структур с использованием обозначения %MyStruct{...}. Эта оптимизация также недоступна, если структура имеет 32 ключа или более.

Рефакторинг

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

  • Если структура имеет «необязательные» поля, например, поля, инициализированные значением nil, вы можете вложить все необязательные поля в другое поле, называемое :metadata, :optionals, или подобное. Это может привести к преимуществам, таким как возможность использования сопоставления с образцом для проверки наличия поля или его отсутствия вместо полагания на значения nil

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

  • Вы можете вложить поля в виде кортежей. Например, если два поля всегда читаются или обновляются вместе, их можно перенести в кортеж (или другую составную структуру данных).

Задача состоит в том, чтобы сбалансировать вышеуказанные изменения с эргономикой API, в частности, когда к полям может быть частый доступ для чтения и записи.

← Предыдущая страница Что такое анти-паттерны?
Следующая страница → Анти-паттерны, связанные с проектированием

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

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

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

Spec-Zone.ru

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