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