Кодогенерация на dart

27 сентября 2021 г. 17:27

Dart - довольно молодой, легкий в освоении язык программирования. Подавляющее большинство из нас приходят к нему исключительно ради разработки на Flutter. И оно понятно. Ведь это реактивный кроссплатформенный фреймворк с отличной документацией от корпорации Google и long time support.

Однако его "молодость" все же накладывает отпечаток на разработку. Вероятно, местами dart может оказаться не настолько гибким, насколько бы хотелось. Не настолько, насколько это возможно в других языках, что местами может приводить к написанию избыточного шаблонного кода, и, как следствие, нарушению основополагающего принципа DRY (don't repeat yourself).

Тем не менее, язык развивается, и, возможно, в ближайшем будущем догонит своих старших братьев.

Возможно, если вы читаете эту статью из будущего, то недоумеваете о чем речь. Но сейчас в 2021 году в версии 2.14.1 в dart нет partial классов, возможности перегрузки оператора преобразования типов, наследования статических методов, инициализации полей по умолчанию и много другого. Про многие из эти "фич" можно сказать, что они не нужны, но неплохо было бы, если бы многие из них и были. К счастью разработчики dart предусмотрели несколько утилит, которые позволяют творить удивительные вещи - такие вещи, которые могут нивелировать недоработки языка. И одна из таких удивительных вещей - это кодогенерация.

Кодогенерация позволяет дать вашему коду сверхспособности и приблизить будущие возможности языка. И если вы не боитесь будущего, добро пожаловать под кат...

Кодогенерация

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

Лирическое отступление

Для начала пару слов о том, что мы будем генерировать

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

class C {
  C(int? a){
    this.a = a;
  }
  int? a;
}
мы можем использовать короткий синтаксис:

class C {
  C(this.a);
  int? a;
}
Удобно, не правда ли? Однако представьте, что у такого класса много полей?

class C {
  C({this.a, this.b, this.c, this.d, this.e, this.f, this.g, this.h});
  int? a;
  int? b;
  int? c;
  int? d;
  int? e;
  int? f;  
  int? g;  
  int? h;  
}

Согласно dart design каждое из этих полей мы должны прописать том или ином виде в аргументы конструктора, если хотим иметь возможность его инициализировать при создании экземпляра. В противном случае нам придется для инициализации каждого поля писать отдельную строку кода:

var c = C();
c.a = 1;
c.b = 2;
c.c = 3;
// etc...

Если вспомнить, что у dart есть замечательный сахар в виде двух точек, то можно было бы предположить, что мы можем сделать так:

var c = C()..a = 1..b = 2;

Пусть выглядит странно, но эффективно (мало кода). Однако, как выясняется, для обычных полей это не работает (работает только методов и геттеров), а значит даже такой "лайфхак" отпадает.

А что же у старших "братьев", golang и C#?

Чтобы раскрыть некоторые утерянные возможности в dart, глянем немного "налево". Там мы увидим, что в C# есть такая фича, как инициализатор класса:

class Program
    {
        class A
        {
            public int a;
            public int b;
            public int c { get; set; }
        }
        static void Main(string[] args)
        {
            var a = new A { a = 1, b = 2, c = 3 };
        }
    }

Как видите, не нужно в конструкторе (в примере он даже не объявлен) описывать инициализацию каждого поля. В этом коде нет ничего лишнего для того, чтобы инициализировать поля объекта. Аналогично это работает и в golang, в котором решили даже не вводить понятие "конструктор".

Но в dart нет инициализатора полей, нет в нем и сахарного объявления свойств, как в листинге выше. И да, в конце концов, в dart поля - это поля, а не свойства, как в kotlin.

Что же делать? Неужели опустить руки и писать так, как предлагают нам разработчики dart?

Не знаю, как вы, а я не люблю серые будни и нудную, однообразную работу. Поэтому после перебора всех перечисленных вариантов и не был готов сдаваться.

Когда я понял, что кодогенерация открывает безграничные (почти) возможности, я подумал, почему бы нам для каждого класса просто не объявить extension, который будет ожидать поля класса. При этом не нужно будет писать конструктор для этих полей руками. Не знаю, как вам, а мне этот вариант показался очень удобным. Ведь теперь можно писать, например, так:

class C{
  int? a;
  int? b;
  int? c;
}
var c = C()._(a: 1, b: 2, c: 3);

Вместо

class C{
  int? a;
  int? b;
  int? c;
}
var c = C();
c.a = 1;
c.b = 2;
c.c = 3;

Вам тоже по душе? Тогда поехали дальше...

Вернемся к нашим "баранам"

Откроем существующий или создадим новый проект, для которого будем генерировать код (далее - "рабочий проект"). Создадим файл в корне директории lib с именем some.dart со следующим содержимым:

part "some.g.dart";
@constructor 
class State{
    int count = 0;
}

Не обращаем пока внимания на ошибки. Их должно быть две: не найден файл some.g.dart и не найдено определение аннотации @constructor.

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

  • Новый проект должен лежать в одной папке (директории) с рабочим проектом

Устанавливаем зависимости. Для начала нам надо установить в только что созданный проект пакеты. Их три:

build_runner: ^2.0.0
source_gen:
build_config:

  • source_gen - главная зависимость. Своего рода фреймворк для кодогенерации. Можно вместо него использовать build, над которым он является удобной оберткой, но я предпочту собственно обертку :). Ложим ее в dev_dependencies.

  • build_config - пакет, читающий конфиг, который отвечает за то, из чего именно и что именно нужно сгенерировать. Так же ложим его в dev_dependencies.

  • build_runner - собственно нужен для запуска процесса кодогенерации. Можно положить в dependencies (в отличие от пакетов в dev_dependencies будет доступен всем использующим наш пакет пакетам).

Для установки вышеуказанных пакетов прописываем их в pubspec.yaml, как на картинке ниже:

И запускаем команду

flutter run pub get

которая их установит в наш проект.

Далее создадим в корне нового проекта файл build.yaml, в котором будем писать наш конфиг. Конфиг у нас будет простейший:

builders:
  constructor_generator:
    import: "package:project_name/builder.dart"
    builder_factories: ["constructorGenerator"]
    build_extensions: {}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

Вместо constructor_generator может быть любое уникальное имя

Вместо [project_name] пишем имя нашего настоящего проекта (совпадает с именем корневой директории проекта).

После создаем файл builder.dart, который будет содержать всю логику кодогенерации расширения. Обратите внимания, что именно на этот файл мы сослались в директиве import в нашем конфиге. Этот файл будет содержать точку входа - в нашем случае функцию constructorGenerator, имя которой мы указали для опции builder_factories (обратите внимание, что у одного builder может быть несколько точек входа)

Итак, объявим функцию constructorGenerator в файле builder.dart:

Builder constructorGenerator(BuilderOptions options) => SharedPartBuilder([ConstructorGenerator()], 'constructor_generator');

Здесь мы будем использовать для генерации Builder-а функцию SharedPartBuilder из пакета source_gen, которая генерирует код для разделяемых модулей, а не LibraryBuilder, которая бы сгенерировала самостоятельный модуль

Рядом объявим класс ConstructorGenerator, унаследованный от Generator (т.к. мы будем делать кодогенерацию из аннотированных классов, можно было бы использовать GeneratorForAnnotation, но мы в образовательных целях возьмем именно Generator) и переопределим в нем метод generate, как тут.

Как вы, вероятно, догадались, этот метод возвращает нам тот самый сгенерированный код. Комментировать сам код внутри generate, я полагаю, не имеет особого смысла, т.к. предполагается, что читатель знаком с основами языка dart (иначе вряд ли бы дошел до понимания необходимости кодогенерации).

И последнее, что нам необходимо сделать - создать файл с аннотациями, annotations.dart рядом с builder.dart со следующим содержимым:

class Constructor
{
    const Constructor();
}
const constructor = Constructor();

После того, как мы это сделали, вернемся к рабочему проекту. Откроем файл pubspec.yaml и пропишем в dependencies ссылку на наш пакет с кодогенерацией:

project_name:
        path: ../project_name/

где вместо project_name - имя нашего проекта с кодом генерации, который лежит рядом.

Теперь можно вернуться к файлу some.dart и указать import для аннотации constructor (alt + enter если вы используете android studio или ctrl + . - если vscode сделают это автоматически). Если зависимость не будет найдена, попробуйте выполнить pub get. После проделанных операций у вас должна остаться только одна ошибка: об отсутствующем файл some.g.dart.

И теперь все, что осталось сделать, это запустить кодогенерацию в нашем рабочем(!) проекте:

flutter clean && flutter packages pub get && flutter packages pub run build_runner build

Если вы все сделали правильно, то увидите в терминале примерно следующее:

По окончанию выполнения скрипта рядом с нашим файлом some.dart должен сгенерироваться файл some.g.dart с содержимым like this:

part of 'state.dart';
// 
// ConstructorGenerator
// 
extension StateConstructor on State {
  void _({int? count}) {
    if (count != null) this.count = count;
  }
}

Отлично. Теперь вы можете спокойно использовать конструкцию:

var state = State().._(count: 5);

Неплохо, правда? Запустив команду flutter packages pub run build_runner watch, вы можете спокойно писать код, и extensions будут генерироваться сами при любом изменении класса State. Ну разве не супер?

Хорошо. Теперь вы владеете основами кодогенерации на dart и можете внести в свою жизнь новые краски безграничных (почти) возможностей. А у нас на этом все. Исходники доступны в репозитории на гитхаб

Всем удачи и до новых встреч!

admin
1
(ваш голос учтен)