Исходный код Антипаттерны метапрограммирования
Этот документ описывает потенциальные антипаттерны, связанные с метапрограммированием.
Генерация большого объёма кода
Проблема
Этот антипаттерн связан с макросами, генерирующими слишком много кода. Когда макрос генерирует большое количество кода, это влияет на работу компилятора и/или среды выполнения. Причина в том, что 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.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/macro-anti-patterns.html