С возрастом я обнаружил, что пытаюсь запомнить приемы кодирования и идиомы, которые не должны быть сложными для запоминания, но иногда все равно запоминаются. Это один из таких случаев — атрибуты модуля потребовали от меня некоторых усилий, чтобы разобраться с ними, поэтому я решил записать их. Я делюсь этим здесь, чтобы другие могли извлечь из этого пользу.
Что я имею в виду под константой?
Прежде всего, уместно сказать, что когда я говорю «константа», я имею в виду определенный технический смысл. В математическом смысле константа, такая как pi или e, — это просто величина, фиксированная на числовой прямой. В том смысле, в котором разработчики программного обеспечения используют слово «константа», мы обычно имеем в виду нечто, что является расширением понятия математической константы — фиксированное значение. Это может быть число, строка или даже более сложная структура данных; суть в том, что в ходе обычного выполнения двоичного файла она не изменится.
Здесь я использую термин константа, чтобы назвать значение, на которое нужно ссылаться в двух или более модулях Elixir, и ни один из них не изменится. Значение будет определено в одном модуле, а затем прочитано из других. Есть две причины для такого подхода:
- Это делает код более читабельным
- Это удерживает разработчика от повторений.
Обе эти причины хороши для использования такого подхода. Я собираюсь немного рассмотреть их, прежде чем обсуждать конкретно технику кодирования.
Более читабельный код
В то время как компилятору и/или интерпретатору нет дела до разницы между кодом с использованием литералов и кодом с использованием символьных имен, разработчикам это очень важно. Если у меня есть код, который выглядит следующим образом:
def is_odd_number?(number), do: rem(number,2) != 0 #Version 1
vs.
def f(n), do: !(rem(n,2) == 0) #Version 2
какой из них лучше передает замысел программиста? Сейчас вы можете сказать: «Эй, никто не называет свои функции f и никто не называет свои аргументы n», и я хотел бы сказать, что вы правы. Но я сталкивался с подобным кодом на практике, и его трудно расшифровать — во всяком случае, человеку. Компилятору все равно. Дело в том, что использование описательных имен (в отличие от простых литералов) — это отличная техника для того, чтобы сделать ваш код более читабельным.
Константы, в этом смысле, являются формой описательного имени для величины.
Возможно, через полгода вам придется перечитывать свой собственный код, и вы будете рады, что потратили дополнительное время на то, чтобы сделать его более понятным.
Удерживает разработчика от повторений
Это стало мантрой, которую снова и снова повторяют разработчики, которые мало что знают, но они знают, что повторять что-либо в коде — плохая идея. Но это не всегда плохая идея. Фокус в том, чтобы понять, когда можно повторять, а когда нет.
Частично отличить один случай от другого можно, если понять, почему повторение вообще плохо. Проблема в том, что если я дублирую код, а потом этот код нужно исправить, то велика вероятность, что я не найду все дубликаты и не исправлю их тоже. И это при условии, что я работаю над своим собственным кодом. Еще более вероятно, что это будет проблемой, когда кто-то другой будет поддерживать ваш код. Они с большей вероятностью пропустят места, где код нуждается в исправлении.
Поэтому в следующий раз, когда у вас возникнет соблазн скопировать/вставить часть кода, подумайте о своем будущем (или, возможно, о бедном человеке, который будет поддерживать ваш код) и сделайте небольшой рефакторинг, чтобы не повторять код.
Бывают случаи, когда повторение кода действительно упрощает работу. Часть проблемы с устранением дублирования заключается в том, что оно вносит сцепление. Пока ваша связь чистая и простая (в декомпилированном смысле слова «простая»), вы в порядке. Общие определения — числовые константы, строки, обозначающие имена, и т.д. — определенно попадают под эту рубрику.
Однако код, находящийся на сопровождении, имеет тенденцию получать много «это почти то, что мне нужно, но не совсем». Это часть причины, по которой люди склонны копировать/вставлять код в первую очередь. Поэтому совет избегать повторения кода не является абсолютным, и вы должны применять свое суждение. Часто ли меняется место, куда вы думаете скопировать/вставить код? Могут ли эти изменения отличаться от изменений исходного кода? Тогда, конечно, копируйте/вставляйте. С другой стороны, если это место вряд ли будет часто меняться, то вам лучше ссылаться на одно определение из других мест вашего кода. Примером того, что может меняться, но вряд ли будет меняться часто, является название компании. Компания может изменить свое название, но вряд ли это будет происходить часто. Если вы жестко закодировали это название в 50 местах вашего кода, это 50 мест, которые вам придется найти и исправить. Если вы жестко закодировали его в одном месте, а затем ссылаетесь на эту константу в другом коде, то вам нужно исправить ее только в одном месте.
Частично решение о том, что следует повторять, а что нет, сводится к связности, то есть к тому, насколько код в определенном блоке кода выполняет одну функцию. Если часть кода обладает высокой связностью (например, функция для вычисления площади круга) по сравнению с функцией с низкой связностью (например, вычислить площадь круга, распечатать круг тремя цветами, а затем вернуть текущую дату), то лучше не повторять этот код. Наше суждение о копировании и вставке кода должно быть обратно пропорционально силе связности кода.
Как и многие другие идеи в инженерии, понятие «не повторяйся» не является тем, которое можно применять без раздумий и суждений.
Константы в Elixir
Теперь, после всего сказанного, давайте поговорим о нескольких способах работы со значениями, разделяемыми между модулями в Elixir. Вы можете зарегистрировать атрибут в одном модуле и затем ссылаться на него в других модулях или создать (за неимением лучшего названия) константную функцию — то есть функцию, которая не принимает никаких аргументов и просто возвращает постоянное значение. Если кто-то знает правильное название для такой функции, пожалуйста, напишите мне его в комментариях! Для наглядности я представлю, что мне нужна константа Pi в моем коде.
Рассмотрим следующий код:
defmodule MyMath do
Module.register_attribute(__MODULE__, :pi, persist: true)
@pi 3.141592653
def pi, do: 3.141592653
# this could also be:
# def pi, do: @pi
end
и теперь у меня есть два способа получить значение Pi в других модулях:
defmodule My.Other.Module.That.Needs.To.Know.Pi do
defp get_pi do # module attribute approach
[pi] = MyMath.__info__(:attributes)[:pi]
pi
end
def circle_area(radius) do
MyMath.pi() * radius * radius #constant function approach
end
# or
def circle_area_alternate(radius) do
get_pi() * radius * radius
end
end
Оба этих подхода будут работать. Подход с атрибутами действительно не рекомендуется для константы, разделяемой между модулями. Он больше подходит для метаданных о модуле — автор, дата последней ревизии и т.д. Более рекомендуемый способ сделать это (хотя я согласен, что это выглядит немного странно) — это функция константы.