Spec-Zone.ru › Elixir 1.16

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

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

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

Проблема

Этот антипаттерн связан с макросами, которые генерируют слишком много кода. Когда макрос генерирует большое количество кода, это влияет на работу компилятора и/или среды выполнения. Причина в том, что 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

use вместо import

Проблема

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.32.2) для языка программирования Elixir

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

Spec-Zone.ru

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