Выбрать главу

  {% end %}

end

def value(obj) i : NoReturn

    raise "BUG: Invoked default value method."

end

Другая хитрость, которая делает эту реализацию возможной, — это возможность прямого доступа к переменным экземпляра типа, даже если у них нет метода получения через синтаксис obj.@ivar_name. В предисловии к этому я скажу, что вам не следует использовать это часто, если вообще когда-либо, за исключением очень специфических случаев использования, таких как этот. Это антишаблон, и его следует избегать, когда это возможно. В 99% случаев вам следует вместо этого определить метод получения, чтобы вместо этого предоставить значение переменной экземпляра.

С учетом вышесказанного реализация использует индекс переменной экземпляра для доступа к ее имени и использования его для создания предыдущего синтаксиса. Поскольку все это происходит во время компиляции, фактический метод, который добавляется, например, для переменной экземпляра name, будет выглядеть следующим образом:

def value(obj : ClassType)

    obj.@name

end

Мы также определили еще одну перегрузку, которая вызывает исключение, если вы передаете экземпляр объекта, тип которого отличается от типа, представленного экземпляром метаданных. В основном это делается для того, чтобы компилятор был доволен, когда существует более одного типа Metadatable. На практике этого никогда не должно происходить, поскольку конечный пользователь не будет напрямую взаимодействовать с этими экземплярами метаданных, поскольку это будет внутренней деталью реализации.

Мы можем пойти дальше и опробовать это, добавив в нашу программу следующее и запустив ее:

my_class = MyClass.new

pp MyClass.metadata["name"].value my_class

Вы должны увидеть значение свойства name, напечатанное на вашем терминале, которое в данном случае будет "Jim". У этой реализации есть один недостаток. Тип значения, возвращаемого методом #value, будет состоять из объединения всех свойств, имеющих аннотацию данного типа. Например, typeof(name_value) вернет (String | Time), что в целом приводит к менее эффективному представлению памяти.

Этот шаблон отлично подходит для реализации мощных внутренних API, но его следует использовать с осторожностью, не использовать в «горячем» пути приложения и даже не публиковать публично.

Если вы помните Главу 9 «Создание веб-приложения с помощью Athena», где вы применяли аннотации ограничений проверки, компонент Validator Athena реализован с использованием этого шаблона, хотя и с несколько большей сложностью.

Конечно, это, скорее всего, не тот шаблон, который вам понадобится очень часто, если вообще когда-либо понадобится, но полезно знать, если такая необходимость когда-нибудь возникнет. Это также хороший пример того, насколько мощными могут быть макросы, если вы мыслите немного нестандартно. В качестве дополнительного бонуса мы можем еще раз продвинуть эту модель на шаг дальше.

Моделирование всего класса

В предыдущем разделе мы рассмотрели, как можно использовать структуру для представления определенного элемента, например переменной экземпляра или метода, вместе с данными из примененной к нему аннотации. Другой шаблон предполагает создание специального типа для хранения этих данных вместо непосредственного использования массива или хеша. Этот шаблон может быть полезен для отделения метаданных о типе от самого типа, а также для добавления дополнительных методов/свойств без необходимости засорять фактический тип.

Чтобы это работало, вам нужно иметь возможность перебирать свойства и создавать хэш или массив внутри конструктора другого типа. Несмотря на то, что существует ограничение на чтение переменных экземпляра типа, оно не означает, что это должен быть метод внутри самого типа. Учитывая, что конструктор — это всего лишь метод, который возвращает self, это не будет проблемой. Несмотря на это, нам все равно нужна ссылка на TypeNode интересующего нас типа.

Поскольку макросы имеют доступ к общей информации, даже в контексте метода мы можем заставить этот тип ClassMetadata принимать аргумент универсального типа, чтобы передать ссылку на TypeNode. Кроме того, мы могли бы продолжать передавать общий тип другим типам/методам, которым он нужен.

Например, используя тот же тип PropertyMetadata, что и в последнем разделе:

annotation Metadata; end

annotation ClassConfig; end

class ClassMetadata(T)

  def initialize

    {{@type}}

    {% begin %}

      @property_metadata = {