Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
Когда у нас есть TypeNode, мы можем использовать два метода в зависимости от того, что именно мы хотим сделать. TypeNode#subclasses можно использовать для получения прямых подклассов этого типа. TypeNode#all_subclasses можно использовать для получения всех подклассов этого типа, включая подклассы подклассов и так далее. Например, добавьте в файл следующие две строки вместе с показанным ранее деревом наследования:
{{pp Vehicle.subclasses}}
{{pp Vehicle.all_subclasses}}
В результате компиляции программы на консоль будут выведены две строки: первая — [Car, SUV], а вторая — [Car, Sedan, Van, SUV]. Вторая строка длиннее, поскольку она также включает подклассы типа Car, который не включен в первую строку, поскольку Van и Sedan не являются прямыми дочерними элементами типа Vehicle.
Также обратите внимание, что массив содержит как конкретные, так и абстрактные типы. На это стоит обратить внимание, поскольку если бы вы захотели перебрать типы и создать их экземпляры, это не удалось бы, поскольку был бы включен абстрактный тип Car. Чтобы этот пример работал, нам нужно отфильтровать список типов до тех, которые не являются абстрактными. Оба метода в предыдущем примере возвращают ArrayLiteral(TypeNode). По этой причине мы можем использовать метод ArrayLiteral#reject для удаления абстрактных типов. Код для этого будет выглядеть так:
{% for type in Vehicle.all_subclasses.reject &.abstract? %}
pp {{type}}.new
{% end %}
Запуск этого в конечном итоге приведет к печати нового экземпляра типов Sedan, Van, и SUV. Мы можем пойти дальше в этой идее фильтрации и включить более сложную логику, например, использование данных аннотаций для определения того, следует ли включать тип.
Например, предположим, что мы хотим получить подмножество типов, имеющих аннотацию, исключая те, у которых есть определенное поле аннотации. В этом примере мы будем использовать следующие типы:
annotation MyAnnotation; end
abstract class Parent; end
@[MyAnnotation(id: 456)]
class Child < Parent; end
@[MyAnnotation]
class Foo; end
@[MyAnnotation(id: 123)]
class Bar; end
class Baz; end
У нас пять занятий, включая одно реферативное. Мы также определили аннотацию и применили ее к некоторым типам. Кроме того, некоторые из этих аннотаций также включают поле id, в котором установлено некоторое число. Используя эти классы, давайте переберем только те, у которых есть аннотация и либо нет поля id, либо ID является четным числом.
Однако обратите внимание, что в отличие от предыдущих примеров здесь нет прямого родительского типа, от которого наследуются все типы, а также не существует конкретного модуля, включенного в каждый из них. Итак, как мы собираемся отфильтровать нужный нам тип? Здесь в игру вступает звездочка в начале главы. Пока не существует прямого способа просто получить все типы с определенной аннотацией. Однако мы можем использовать один и тот же шаблон перебора всех подклассов типа, чтобы воспроизвести это поведение.
Итерация типов с определенной аннотацией
В Crystal Object является самым верхним типом из всех типов. Поскольку все типы неявно наследуются от этого типа, мы можем использовать его в качестве базового родительского типа для фильтрации до нужных нам типов.
Однако, поскольку этот подход требует перебора всех типов, он гораздо менее эффективен, чем более целенаправленный подход. В будущем, возможно, появится лучший способ сделать это, но на данный момент, в зависимости от конкретного варианта использования/API, который вы хотите поддерживать, это достойный обходной путь.
Например, этот подход необходим, если типы, которые вы хотите перебрать, еще не имеют какого-либо общего определяемого пользователем типа и/или включенного модуля. Однако, поскольку этот тип также является родительским типом для типов в стандартной библиотеке, вам потребуется какой-то способ его фильтровать, например, с помощью аннотации.
Код, фактически выполняющий фильтрацию, похож на предыдущие примеры, только с немного более сложной логикой фильтрации. В конечном итоге это будет выглядеть следующим образом:
{% for type in Object.all_subclasses.select {|t| (ann =
t.annotation(MyAnnotation)) && (ann[:id] == nil || ann[:id]
% 2 == 0) } %}
{{pp type}}
{% end %}
В этом случае мы используем ArrayLiteral#select, потому что нам нужны только те типы, для которых этот блок возвращает true. Логика отражает требования, которые мы упоминали ранее. Он выбирает типы, которые имеют нашу аннотацию и либо не имеют поля id, либо поля id с четным номером. При создании этого примера будут правильно напечатаны ожидаемые типы: Child и Foo.
Итерационные типы, включающие определенный модуль
Третий способ, которым мы можем перебирать типы, - это запросить те типы, которые включают определенный модуль. Это может быть достигнуто с помощью метода TypeNode#includers, где TypeNode представляет модуль, например:
module SomeInterface; end
class Bar
include SomeInterface
end
class Foo; end
class Baz
include SomeInterface
end
class Biz < Baz; end
{{pp SomeInterface.includers}}
Построение этой программы выведет следующее:
[Bar, Baz]
При использовании метода #includers следует отметить, что он включает только типы, которые напрямую включают этот модуль, а не типы, которые затем наследуются от него. Однако затем можно было бы вызвать #all_subclasses для каждого типа, возвращаемого через #includers, если это соответствует вашему варианту использования. Конечно, здесь также применима любая из ранее упомянутых логик фильтрации, поскольку #includers возвращает ArrayLiteral(TypeNode).
Во всех этих примерах мы начали с базового родительского типа и прошли через все подклассы этого типа. Также возможно сделать обратное; начните с дочернего типа и перебирайте его предков. Например, давайте посмотрим на предков класса Biz, добавив в нашу программу следующий код и запустив его:
{{pp Biz.ancestors}}
Это должно вывести следующее:
[Baz, SomeInterface, Reference, Object]
Обратите внимание, что мы получаем прямой родительский тип, модуль, который включает в себя его суперкласс, и некоторые неявные суперклассы этого типа, включая вышеупомянутый тип Object. И снова метод #ancestors возвращает ArrayLiteral(TypeNode), поэтому его можно фильтровать, как мы это делали в предыдущих примерах.
Следующая особенность метапрограммирования, которую мы собираемся рассмотреть, — это