Если вы работали над проектами Flutter (или вы Java-разработчик), вы можете быть знакомы с библиотекой Mockito.
Mockito позволяет разработчикам создавать объекты-макеты из существующих классов, заглушки их методов и проверять или утверждать их поведение во время тестирования.
Если вы использовали Mockito для проектов Flutter или Dart, то в их документации вы найдете следующее:
Чтобы использовать сгенерированные Mockito mock-классы, добавьте зависимость build_runner в файл pubspec.yaml вашего пакета в разделе dev_dependencies; что-то вроде build_runner: ^1.11.0.
В некоторых примерах кода будет показано, как высмеять существующий класс, например Cat
, следующим образом:
@GenerateMocks([ Cat ])
Затем ожидается, что вы выполните следующую команду:
flutter pub run build_runner build
Команда build_runner build
создает файл под названием *.mocks.dart
. Что именно здесь происходит?
Отражение (и его отсутствие)
FAQ по Flutter содержит следующий вопрос:
***Поставляется ли Flutter с системой отражения/зеркал? ***Нет. Dart включает dart:mirrors, который обеспечивает отражение типов. Но поскольку приложения Flutter предварительно компилируются для производства, а размер двоичных файлов всегда является проблемой для мобильных приложений, эта библиотека недоступна для приложений Flutter. Используя статический анализ, мы можем удалить все, что не используется («tree shaking»)… Эта гарантия надежна только в том случае, если Dart может определить путь кода во время компиляции. На сегодняшний день мы нашли другие подходы для конкретных нужд, которые предлагают лучший компромисс, например, генерацию кода.
Согласно Википедии, отражение — это:
В информатике рефлексивное программирование или рефлексия — это способность процесса исследовать, анализировать и изменять свою собственную структуру и поведение.
В других языках, таких как Java, библиотеки типа Mockito используют отражение для получения информации во время выполнения о классах, которые они модифицируют. Например, функция Java может получить все доступные методы класса с помощью следующего кода:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
В этом примере функция получает массив, содержащий все функции, которые являются частью класса java.lang.String
.
Мы видим, как это может быть полезно для таких библиотек, как Mockito: она может найти все методы заданного класса и обеспечить реализацию заглушек на основе их типов возврата и параметров. И все это во время выполнения.
Обычный подход заключается в том, чтобы пометить классы аннотациями, затем найти эти классы с помощью рефлексии и что-то с ними сделать:
- Создать имитационные реализации.
- Инстанцировать синглтоны для класса.
- Создавать и внедрять экземпляры других классов в конструктор (как это делает фреймворк Spring).
- Украшать классы для обеспечения такого поведения, как сериализация JSON или представление строк, и так далее.
Хорошим примером использования отражения является фрагмент Java, который мы включили в предыдущую статью «Легкий параллелизм и многопоточность с помощью CompletableFuture от Java» (обновлено, чтобы удалить общие классы для ясности):
private static <Book> List<Book> parseJSON(String textResponse) {
final ObjectMapper objectMapper = new ObjectMapper();
List<T> objects = new ArrayList<>();
try {
objects =
objectMapper.readValue(textResponse, new TypeReference<List<Book>>() {});
} catch (JsonMappingException e) {
// TODO: Do something with the error
} catch (JsonProcessingException e) {
// TODO: Do something with the error
e.printStackTrace();
}
return objects;
}
В этом примере кода Jackson mapper использует отражение, чтобы узнать тип класса, который он должен использовать для де-сериализации строки JSON:
List<T> objects = objectMapper.readValue(textResponse, new TypeReference<List<Book>>() {});
Параметр new TypeReference<List<Book>>() {}
создает ссылку на класс, которая позволяет Jackson’sObjectMapper
знать, что он должен разобрать JSON и создать экземплярList<Book>
.
Однако мы не можем выполнить подобную де-сериализацию JSON во Flutter. Без рефлексии нам нужно рассмотреть другие подходы для решения этих же задач. Например, пакет json_serializable
использует генерацию исходного кода для оформления классов с методами для сериализации и де-сериализации строк JSON.
Генерация исходного кода
Когда у нас нет доступа к метаданным о классах во время выполнения, одной из альтернатив является интроспекция во время компиляции.
В то время как Flutter ограничивает отражение во время выполнения, такие инструменты, как source_get, полагаются на два низкоуровневых пакета Dart:
- build: «Определяет основные части того, как происходит сборка и как они взаимодействуют».
- analyzer: «Этот пакет предоставляет библиотеку, выполняющую статический анализ кода Dart. Она полезна для интеграции и встраивания инструментов.»
Build предоставляет интерфейс для реализации билдеров: классов, которые обеспечивают точку входа в процесс компиляции, позволяя нам создавать файлы в результате нашего исходного кода.
Пакет analyzer предоставляет инструменты для получения информации о наших классах: Методы и переменные, которые они содержат, аннотации, которые они используют, среди прочего.
Инструмент build_runner
(который упоминается в документации Mockito) использует эти два низкоуровневых пакета для генерации исходного кода.
Высокоуровневый процесс: json_serializable
На высоком уровне пакет json_serializable
выполняет следующие действия:
- Поручить разработчикам аннотировать классы с помощью
@JsonSerializable
. Аннотированный класс должен соответствовать набору рекомендаций, чтобы процесс работал правильно. - Пусть разработчики запустят инструмент
build_runner
для генерации кода, который позволит этим классам сериализовать и де-сериализовать строки JSON.
На более низком уровне процесс генерации исходного кода происходит следующим образом:
- Разработчики выполняют инструмент
build_runner
и, в свою очередь,build_runner
выполняет классы построителяjson_serializable
.- Строитель использует
build
для определения того, какие файлы он будет читать из исходного кода, и какие файлы он будет генерировать. - Строитель использует
analyzer
для поиска всех классов, аннотированных@JsonSerializable
. - Конструктор использует
analyzer
, чтобы найти все атрибуты и конструкторы для сериализуемых классов. - Конструктор создает классы Dart для сериализации и десериализации строк JSON в атрибуты, найденные с помощью
analyzer
. - Конструктор очищает и помещает эти новые классы в тот же пакет, что и аннотированные классы, в виде файлов
.g.dart
. - Эти файлы
.g.dart
являются частями, что позволяет разделить класс на несколько файлов.
- Строитель использует
Следуя примеру из документации json_serializable
, мы аннотируем следующий класс, содержащийся в example.dart
:
@JsonSerializable()
class Person {
/// The generated code assumes these values exist in JSON.
final String firstName, lastName;
/// The generated code below handles if the corresponding JSON value doesn't
/// exist or is empty.
final DateTime? dateOfBirth;
Person({required this.firstName, required this.lastName, this.dateOfBirth});
/// Connect the generated [_$PersonFromJson] function to the `fromJson`
/// factory.
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
После запуска build_runner
создается следующий файл example.g.dart
:
part of 'example.dart';
Person _$PersonFromJson(Map<String, dynamic> json) => Person(
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
dateOfBirth: json['dateOfBirth'] == null
? null
: DateTime.parse(json['dateOfBirth'] as String),
);
Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
'firstName': instance.firstName,
'lastName': instance.lastName,
'dateOfBirth': instance.dateOfBirth?.toIso8601String(),
};
Все, что нам нужно сделать, это импортировать example.g.dart
в example.dart
и мы получим доступ к приватным функциям _$PersonFromJson
и _$PersonToJson
.
Преимущества и недостатки
Отсутствие отражения заставляет разработчиков четко формулировать типы и случаи использования, которые им необходимо поддерживать. Тем не менее, мы все еще можем полагаться на дженерики для создания многократно используемого кода.
Генерация исходного кода уменьшает количество ручной работы, которую разработчикам приходится выполнять для повторяющихся задач. Без source_gen
и json_serializable
нам пришлось бы вручную создавать функции toJson
и fromJson
, перечисляющие каждый атрибут для каждого класса. Для этого есть альтернативные варианты, использующие дженерики и наследование. Однако это все равно требует значительного объема ручной работы.
Хотя генерация исходного кода сокращает эту ручную работу, она вносит дополнительный уровень сложности в наш проект: Теперь у нас есть большой кусок исходного кода, который не будет доступен для проверки, пока мы не запустим инструмент build
. Мы всегда можем проверить сгенерированный код в системе контроля версий, что поможет лучше отслеживать изменения, сделанные при повторном запуске инструмента build
.
Но главным недостатком является дополнительная сложность. Генераторы исходного кода зависят от наличия в коде очень специфических условий (например, наличие открытых конструкторов, соблюдение соглашений об именах), которые могут привести к сбою процесса сборки, если они не соблюдены. Легко обнаружить, что мы тратим часы своего рабочего дня только на то, чтобы выяснить, что сборщик был настроен неправильно.
Что дальше
В следующей статье блога мы создадим простой плагин для сборки исходного кода для проектов Flutter. Мы начнем углубляться в процесс сборки и в то, как создавать многократно используемые модули, которые могут быть импортированы другими проектами, подобно тому, как это делают json_serializable
или mockito
.