Spec-Zone.ru › Elixir 1.17

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

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

Генерация большого объёма кода

Проблема

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

Пример

Представьте, что вы определяете маршрутизатор для веб-приложения, где у вас могут быть макросы, такие как get/2. При каждом вызове макроса (который может составлять сотни раз), код внутри get/2 будет расширен и скомпилирован, что в целом может сгенерировать большой объём кода.

defmodule Routes do
  defmacro get(route, handler) do
    quote do
      route = unquote(route)
      handler = unquote(handler)

      if not is_binary(route) do
        raise ArgumentError, "route must be a binary"
      end

      if not is_atom(handler) do
        raise ArgumentError, "handler must be a module"
      end

      @store_route_for_compilation {route, handler}
    end
  end
end

Рефакторинг

Чтобы устранить этот антипаттерн, разработчик должен упростить макрос, делегировав часть его работы другим функциям. Как показано ниже, заключив код внутри quote/1 в функцию __define__/3 вместо этого, мы уменьшаем код, который расширяется и компилируется при каждом вызове макроса, а вместо этого передаём задачу функции для выполнения основной части работы.

defmodule Routes do
  defmacro get(route, handler) do
    quote do
      Routes.__define__(__MODULE__, unquote(route), unquote(handler))
    end
  end

  def __define__(module, route, handler) do
    if not is_binary(route) do
      raise ArgumentError, "route must be a binary"
    end

    if not is_atom(handler) do
      raise ArgumentError, "handler must be a module"
    end

    Module.put_attribute(module, :store_route_for_compilation, {route, handler})
  end
end

Необходимые макросы

Проблема

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

Пример

Модуль MyMath реализует макрос sum/2 для вычисления суммы двух чисел, полученных в качестве параметров. Хотя в этом коде нет синтаксических ошибок и он может быть выполнен для получения желаемого результата, он излишне сложен. Реализовав эту функциональность как макрос вместо обычной функции, код стал менее понятным:

defmodule MyMath do
  defmacro sum(v1, v2) do
    quote do
      unquote(v1) + unquote(v2)
    end
  end
end
iex> require MyMath
MyMath
iex> MyMath.sum(3, 5)
8
iex> MyMath.sum(3 + 1, 5 + 6)
15

Рефакторинг

Чтобы устранить этот антипаттерн, разработчик должен заменить ненужный макрос более простыми и понятными структурами, такими как именованные функции. Приведённый ниже код является результатом рефакторинга предыдущего примера. В основе лежит преобразование макроса sum/2 в обычную именованную функцию. Обратите внимание, что вызов require/2 больше не нужен:

defmodule MyMath do
  def sum(v1, v2) do # <= The macro became a named function
    v1 + v2
  end
end
iex> MyMath.sum(3, 5)
8
iex> MyMath.sum(3+1, 5+6)
15

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

Проблема

Elixir имеет механизмы, такие как import/1, alias/1, и use/1, для установления зависимостей между модулями. Сам по себе код, реализованный с помощью этих механизмов, не является признаком антипаттерна. Однако, в то время как директивы import/1 и alias/1 обладают лексическим охватом и только позволяют модулю вызывать функции другого модуля, директива use/1 имеет более широкий охват, что может быть проблематично.

Директива use/1 позволяет модулю вставлять любой тип кода в другой, включая распространение зависимостей. Таким образом, использование директивы use/1 усложняет чтение кода, так как для понимания точного поведения при ссылке на модуль необходимо знать внутренние детали этого модуля.

Пример

Приведённый ниже код демонстрирует этот антипаттерн. Он определяет три модуля — ModuleA, Library, и ClientApp. ClientApp использует код из Library через директиву use/1, но не осведомлена о его внутренних деталях. Это затрудняет автору ClientApp представить, какие модули и функциональности теперь доступны в его модуле. Хуже того, Library также импортирует ModuleA, который определяет функцию foo/0, конфликтующую с локальной функцией, определённой в ClientApp.

defmodule ModuleA do
  def foo do
    "From Module A"
  end
end
defmodule Library do
  defmacro __using__(_opts) do
    quote do
      import Library
      import ModuleA  # <= propagating dependencies!
    end
  end

  def from_lib do
    "From Library"
  end
end
defmodule ClientApp do
  use Library

  def foo do
    "Local function from client app"
  end

  def from_client_app do
    from_lib() <> " - " <> foo()
  end
end

При попытке компиляции ClientApp, Elixir обнаруживает конфликт и выдаёт следующую ошибку:

error: imported ModuleA.foo/0 conflicts with local function
  └ client_app.ex:4:

Рефакторинг

Для устранения этого антипаттерна мы рекомендуем авторам библиотек избегать предоставления обратных вызовов __using__/1 всякий раз, когда их можно заменить директивами alias/1 или import/1. В следующем коде мы предполагаем, что use Library больше недоступен, и ClientApp был рефакторинг таким образом, в результате чего код более понятен, а конфликт, как показано ранее, больше не существует:

defmodule ClientApp do
  import Library

  def foo do
    "Local function from client app"
  end

  def from_client_app do
    from_lib() <> " - " <> foo()
  end
end
iex> ClientApp.from_client_app()
"From Library - Local function from client app"

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

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

Поэтому, чтобы обеспечить руководство и ясность, мы рекомендуем авторам библиотек включать блок предупреждений в их @moduledoc, который объясняет, как use MyModule влияет на код разработчика. Например, документация GenServer описывает:

use GenServer

При use GenServer, модуль GenServer установит @behaviour GenServer и определит функцию child_spec/1, что позволит использовать ваш модуль в качестве ребёнка в дереве надзора.

Рассматривайте этот свод как «таблицу пищевой ценности» для генерации кода. Убедитесь, что в нём перечислены только изменения, внесённые в публичный API модуля. Например, если use Library устанавливает внутреннее свойство под названием @_some_module_info, и это свойство никогда не предназначалось для публичного доступа, избегайте его документирования в таблице пищевой ценности.

Для удобства, синтаксис разметки для создания блока предупреждения, показанного выше, выглядит так:

> #### `use GenServer` {: .info}
>
> When you `use GenServer`, the `GenServer` module will
> set `@behaviour GenServer` and define a `child_spec/1`
> function, so your module can be used as a child
> in a supervision tree.
← Предыдущая страница Антипаттерны, связанные с процессами
Следующая страница → Цитаты и удаление цитат

Скачать версию 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/macro-anti-patterns.html

Spec-Zone.ru

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