Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
Если вы помните Главу 9 «Создание веб-приложения с помощью Athena», где вы применяли аннотации ограничений проверки, компонент Validator Athena реализован с использованием этого шаблона, хотя и с несколько большей сложностью.
Конечно, это, скорее всего, не тот шаблон, который вам понадобится очень часто, если вообще когда-либо понадобится, но полезно знать, если такая необходимость когда-нибудь возникнет. Это также хороший пример того, насколько мощными могут быть макросы, если вы мыслите немного нестандартно. В качестве дополнительного бонуса мы можем еще раз продвинуть эту модель на шаг дальше.
Моделирование всего класса
В предыдущем разделе мы рассмотрели, как можно использовать структуру для представления определенного элемента, например переменной экземпляра или метода, вместе с данными из примененной к нему аннотации. Другой шаблон предполагает создание специального типа для хранения этих данных вместо непосредственного использования массива или хеша. Этот шаблон может быть полезен для отделения метаданных о типе от самого типа, а также для добавления дополнительных методов/свойств без необходимости засорять фактический тип.
Чтобы это работало, вам нужно иметь возможность перебирать свойства и создавать хэш или массив внутри конструктора другого типа. Несмотря на то, что существует ограничение на чтение переменных экземпляра типа, оно не означает, что это должен быть метод внутри самого типа. Учитывая, что конструктор — это всего лишь метод, который возвращает self, это не будет проблемой. Несмотря на это, нам все равно нужна ссылка на TypeNode интересующего нас типа.
Поскольку макросы имеют доступ к общей информации, даже в контексте метода мы можем заставить этот тип ClassMetadata принимать аргумент универсального типа, чтобы передать ссылку на TypeNode. Кроме того, мы могли бы продолжать передавать общий тип другим типам/методам, которым он нужен.
Например, используя тот же тип PropertyMetadata, что и в последнем разделе:
annotation Metadata; end
annotation ClassConfig; end
class ClassMetadata(T)
def initialize
{{@type}}
{% begin %}
@property_metadata = {
{% for ivar, idx in T.instance_vars.select &.
annotation Metadata %}
{{ivar.name.stringify}} => (
PropertyMetadata({{@type}}, {{ivar.type.resolve}},
{{idx}}).new({{ivar.name.stringify}},
{{ivar.annotation(Metadata).named_args
.double_splat}})
),
{% end %}
} of String => MetadataBase
@name = {{(ann = T.annotation(ClassConfig)) ?
ann[:name] : T.name.stringify}}
{% end %}
end
getter property_metadata : Hash(String, MetadataBase)
getter name : String
end
Модуль Metadatatable теперь выглядит так:
module Metadatable
macro included
class_getter metadata : ClassMetadata(self)
{ ClassMetadata(self).new }
end
end
Большая часть логики такая же, как и в предыдущем примере, за исключением того, что вместо прямого возврата хеша метод .metadata теперь возвращает экземпляр ClassMetadata, который предоставляет хеш. В этом примере мы также представили еще одну аннотацию, чтобы продемонстрировать, как предоставлять данные, когда аннотацию можно применить к самому классу, например настройку имени с помощью @[ClassConfig(name: "MySpecialName")].
В следующем разделе мы рассмотрим, как можно использовать макросы и константы вместе для регистрации вещей, которые можно будет использовать/перебирать в более поздний момент времени.
Определение значения константы во время компиляции
Константы в Crystal постоянны, но не заморожены. Другими словами, это означает, что если вы определите константу как массив, вы не сможете изменить ее значение на String, но вы можете вставлять/извлекать значения в/из массива. Это, в сочетании с возможностью макроса получать доступ к значению константы, приводит к довольно распространенной практике использования макросов для изменения констант во время компиляции, чтобы впоследствии значения можно было использовать/перебирать в готовом перехватчике.
С появлением аннотаций этот шаблон уже не так полезен, как раньше. Тем не менее, это все равно может быть полезно, если вы хотите предоставить пользователю возможность влиять на некоторые аспекты вашей макрологики, и нет места для применения аннотации. Одним из основных преимуществ этого подхода является то, что его можно вызвать в любом месте исходного кода и при этом применить, в отличие от аннотаций, которые необходимо применять к связанному элементу.
Например, скажем, нам нужен способ регистрации типов во время компиляции, чтобы можно было разрешать их по имени строки во время выполнения. Чтобы реализовать эту функцию, мы определим константу как пустой массив и макрос, который будет помещать типы в константу массива во время компиляции. Затем мы обновим логику макроса, чтобы проверить этот массив и пропустить переменные экземпляра с типами, включенными в массив. Первая часть реализации будет выглядеть так:
MODELS = [] of ModelBase.class
macro register_model(type)
{% MODELS << type.resolve %}
end
abstract class ModelBase
end
class Cat < ModelBase
end
class Dog < ModelBase
end
Здесь мы определяем изменяемую константу, которая будет содержать зарегистрированные типы, сами типы и макрос, который будет их регистрировать. Мы также вызываем #resolve для типа, переданного макросу, поскольку типом аргумента макроса будет Path. Метод #resolve преобразует путь в TypeNode, который представляет собой типы переменных экземпляра. Метод #resolve необходимо использовать только в том случае, если тип передается по имени, например, в качестве аргумента макроса, тогда как макропеременная @type всегда будет TypeNode.
Теперь, когда у нас определена сторона регистрации, мы можем перейти к стороне времени выполнения. Эта часть представляет собой просто метод, который генерирует оператор case, используя значения, определенные в константах MODELS, например:
def model_by_name(name)
{% begin %}
case name
{% for model in MODELS %}
when {{model.name.stringify}} then {{model}}
{% end %}
else
raise "model unknown"
end
{% end %}
end
Отсюда мы можем пойти дальше и добавить следующий код:
pp {{ MODELS }}
pp model_by_name "Cat"
register_model Cat
register_model Dog
pp {{ MODELS }}
pp model_by_name "Cat"
После его запуска вы увидите следующее, напечатанное на вашем терминале:
[]
Cat
[Cat, Dog]
Cat
Мы видим, что первый массив пуст, поскольку ни один тип не был зарегистрирован, хотя строка “Cat" может быть успешно разрешена, даже если после нее зарегистрирован связанный тип. Причина этого в том, что регистрация происходит