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

abstract class Vehicle; end

abstract class Car < Vehicle; end

class SUV < Vehicle; end

class Sedan < Car; end

class Van < Car; end

Первое, что нам нужно, это TypeNode родительского типа, подклассы которого мы хотим перебрать. В нашем случае это будет Vehicle, но это не обязательно должен быть самый верхний тип. Мы могли бы с тем же успехом выбрать Car, если бы она лучше соответствовала нашим потребностям.

Если вы помните первую главу этой части, мы смогли получить TypeNode с помощью специальной макропеременной @type. Однако это будет работать только в том случае, если мы хотим перебирать типы в контексте типа Vehicle. Если вы хотите выполнить итерацию за пределами этого типа, вам нужно будет использовать полное имя родительского типа.

Когда у нас есть 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, который вы хотите поддерживать, это достойный обходной путь.