litbaza книги онлайнРазная литератураCrystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 56 57 58 59 60 61 62 63 64 ... 75
Перейти на страницу:
с осторожностью, не использовать в «горячем» пути приложения и даже не публиковать публично.

Если вы помните Главу 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" может быть успешно разрешена, даже если после нее зарегистрирован связанный тип. Причина этого в том, что регистрация происходит

1 ... 56 57 58 59 60 61 62 63 64 ... 75
Перейти на страницу:

Комментарии
Минимальная длина комментария - 20 знаков. Уважайте себя и других!
Комментариев еще нет. Хотите быть первым?