Эта статья изначально была размещена в нашем блоге.

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

DCM (ранее Dart Code Metrics) — отличный пример такого инструмента. Это инструмент статического анализа, который выявляет проблемы с качеством и согласованностью, помогая вам сократить количество ошибок и быстрее предоставлять результаты вашим пользователям.

Но прежде чем мы начнем, что на самом деле означает статический анализ? Итак, статический анализ — это автоматический анализ исходного кода без запуска приложения. Он ничего не знает о том, как будет выполняться код, с какими фактическими данными он будет работать и т. д. Хорошим примером таких инструментов является анализатор Dart (он у вас, вероятно, уже запущен, даже если вы об этом не знаете). ) и любой инструмент, который является линтером.

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

И да, динамический анализ тоже существует 🙂.

Давайте вместе рассмотрим различные проблемы, с которыми вы можете столкнуться в своем коде, и то, как DCM может помочь вам их найти.

Сетстате

setState имеет простую концепцию (если вы не знали, название было вдохновлено фреймворком React) — он просто уведомляет фреймворк об изменении внутреннего состояния этого объекта.

Но есть несколько случаев, когда вы должны использовать его осторожно.

Избегайте асинхронного разрыва

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

Например,

class _MyWidgetState extends State<MyWidget> {
  String message;

  @override
  Widget build(BuildContext context) {
    return Button(
      onPressed: () async {
        String fromServer = await fetch(...);
        setState(() {
          message = fromServer;
        });
      },
      child: Text(message),
    );
  }
}

здесь fetch может завершиться после того, как виджет уже был удален, что приведет к исключению. Чтобы избежать таких проблем, после любого асинхронного промежутка вам нужно убедиться, что виджет все еще отображается на экране. Это можно сделать через свойство mounted:

class _MyWidgetState extends State<MyWidget> {
  String message;

  @override
  Widget build(BuildContext context) {
    return Button(
      onPressed: () async {
        String fromServer = await fetch(...);
        if (mounted) {
          setState(() {
            message = fromServer;
          });
        }
      },
      child: Text(message),
    );
  }
}

таким образом, вы не получите никаких исключений. Чтобы убедиться, что практика используется, включите use-setstate-synchronously.

Избегайте неуместных мест вызова setState

Вызов setState из методов initState, didUpdateWidget или build (или из любого синхронного метода, вызываемого из них) не нужен и приводит к дополнительному повторному рендерингу (которого вы, вероятно, хотели бы избежать).

Например,

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String myString = '';

  @override
  void initState() {
    super.initState();

    setState(() {
      myString = "Hello";
    });

    if (condition) {
      setState(() {
        myString = "Hello";
      });
    }

    myStateUpdateMethod();
  }

  @override
  void didUpdateWidget(MyWidget oldWidget) {
    setState(() {
      myString = "Hello";
    });
  }

  void myStateUpdateMethod() {
    setState(() {
      myString = "Hello";
    });
  }

  @override
  Widget build(BuildContext context) {
    setState(() {
      myString = "Hello";
    });

    if (condition) {
      setState(() {
        myString = "Hello";
      });
    }

    myStateUpdateMethod();

    return ElevatedButton(
      onPressed: () => myStateUpdateMethod(),
      onLongPress: () {
        setState(() {
          myString = data;
        });
      },
      child: Text('PRESS'),
    );
  }
}

есть семь мест, где вызов setState должен быть удален. Сохранение ненужных вызовов может негативно сказаться на производительности вашего приложения.
Используйте Avoid-Unnecessary-SetState, чтобы всегда быть в курсе любых ненужных вызовов.

Избегайте пустого setState

Пустой setState просто считается анти-шаблоном, и вы всегда должны обновлять состояние внутри переданного обратного вызова (или избегать вызова setState, если нет состояния для обновления). Правило для его применения называется Avoid-empty-setstate.

Визуализировать объекты

Иногда вы попадаете в ситуацию, когда вам нужно создать свои RenderObjects (если вы никогда не слышали о них раньше, прочтите эту замечательную статью).

Есть как минимум две проблемы, о которых вам следует знать: проверки на равенство в сеттерах и правильная реализация updateRenderObject.

Начнем с первого:

class SomeRenderBox extends RenderBox {
  double _dividerWidth;
  double get dividerWidth => _dividerWidth;
  set dividerWidth(double value) {
    _dividerWidth = value;
    markNeedsLayout();
  }
}

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

Для этого всегда проверяйте, отличается ли новое значение от текущего.

class SomeRenderBox extends RenderBox {
  double _dividerWidth;
  double get dividerWidth => _dividerWidth;
  set dividerWidth(double value) {
     if (_dividerWidth == value) return;

    _dividerWidth = value;
    markNeedsLayout();
  }
}

для проверки подобных ошибок используйте правило check-for-equals-in-render-object-setters.

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

class _Decorator extends RenderObjectWidget {
  const _Decorator({
    required this.textDirection,
    required this.isFocused,
    required this.expands,
  });

  final TextDirection textDirection;
  final bool isFocused;
  final bool expands;

  @override
  _RenderDecoration createRenderObject(BuildContext context) {
    return _RenderDecoration(
      textDirection: textDirection,
      isFocused: isFocused,
      expands: expands,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    _RenderDecoration renderObject,
  ) {
    renderObject
      ..expands = expands
      ..textDirection = textDirection;
  }
}

это RenderObjectWidget пропускает поле isFocused в реализации updateRenderObject, что приводит к ошибке, когда поле не обновляется.

Вместо этого updateRenderObject должен выглядеть так:

@override
  void updateRenderObject(
    BuildContext context,
    _RenderDecoration renderObject,
  ) {
    renderObject
      ..expands = expands
      ..textDirection = textDirection
      ..isFocused = isFocused;
  }

Чтобы никогда не забывать обновлять все поля в updateRenderObject включите правило consistent-update-render-object.

Обеспечение правильных типов

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

Методы сбора

К сожалению, несколько методов сбора (например, contains, remove, difference и т. д.) имеют тип, соответствующий параметру Object? (в основном dynamic). Это создает пространство для некоторых неприятных ошибок, когда вы иногда передаете неправильный тип, и нет никакого способа убедиться, что тип правильный, кроме как вручную.

Например,

final map = Map<int, String>();
map.containsKey("str");

здесь у нас есть карта, которая проверяется на наличие в ней ключа «str». Проблема в том, что на карте может быть только int ключей, и единственный способ заметить это — быть очень внимательным.

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

Избегайте динамического типа

Динамический тип создает слишком много места для ошибок, и его рекомендуется избегать как можно дольше (для него есть несколько допустимых вариантов использования, например, при работе с пейлоадами JSON).

Чтобы ваше приложение не зависело от dynamic типов, включите Avoid-Dynamic.

Ненужные возвращаемые типы, допускающие значение NULL

Введение нулевой безопасности в Dart стало огромным шагом вперед для языка и отличным встроенным способом гарантировать, какие части вашей программы на самом деле не могут обрабатывать null. Проблема в том, что если вы пометите весь код как обнуляемый, вы быстро окажетесь в ситуации, когда вы проверяете обнуляемость и пытаетесь создать запасную ветвь кода, которая должна выполняться на нулевом значении, но на самом деле значение, которое вы с которыми мы работаем, вообще не ожидается, что будет null. Это называется ненужным обнуляемым типом.

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

Простой пример:

String? function() {
  return 'srt';
}

здесь function никогда не сможет вернуть null.

Используйте avoid-unnecessary-nullable-return-type, чтобы избежать таких проблем.

Ненужные операции с типами

С предварением выражений is и as возникает проблема, когда вы пытаетесь сравнить тип с самим собой.

Например,

class Example {
  final myList = <int>[1, 2, 3];

  void main() {
    final result = myList is List<int>;
  }
}

здесь myList всегда равно List<int> и проверка is не требуется. Поэтому эта операция не нужна. Чтобы избежать этого, взгляните на избегайте ненужных утверждений типа и избегайте ненужных типов.

Несвязанная операция с типами

Другая проблема, связанная с is и as, заключается в том, что вы можете попытаться использовать их с типами, которые никак не связаны между собой, что приведет к тому, что операция когда-либо вернет false (или выдаст ошибку).

Например,

class Example {
  final regularString = 'some string';

  void main() {
    final result = regularString is int; 
  }
}

здесь regularString никогда не может быть типа int и галочку можно убрать.

Чтобы избежать подобных проблем, используйте avoid-unrelated-type-assertions и avoid-unrelated-type-casts.

Асинхронный код

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

Попробовать / поймать и ждать

Давайте посмотрим на следующий пример:

Future<String> report() async {
  try {
    return anotherAsyncMethod();
  } catch(e) {
    reportError(e);
  }
}

У нас есть асинхронная функция с одним оператором try. Если функция anotherAsyncMethod выдает ошибку, мы хотим, чтобы эта ошибка была обработана reportError. Выглядит просто и очевидно, правда?

Но на самом деле он содержит очень хитрую проблему: поскольку anotherAsyncMethod не ожидается, даже если функция выдает ошибку, catch ее не обработает, и ошибка будет распространяться за пределы функции report.

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

Чтобы избежать необходимости проверять подобные ошибки вручную, вы можете использовать правило под названием prefer-return-await.

Таким образом, правильная версия обрезанного выше:

Future<String> report() async {
  try {
    return await anotherAsyncMethod();
  } catch(e) {
    reportError(e);
  }
}

теперь, даже если функция anotherAsyncMethod выдает ошибку, она будет правильно обработана и передана в reportError.

Повторяющееся асинхронное ключевое слово

Далее, давайте перейдем к другому примеру:

Future<String> parseArgs(Iterable<String> args) async {
  final argsWithDefaultCommand = _addDefaultCommand(args);

  return _parse(argsWithDefaultCommand);
}

Future<String> _parse() {
  ...
}

Этот пример кода снова содержит проблему, можете ли вы ее обнаружить?

Проблема с ключевым словом async. Здесь он действительно не нужен!

В Dart, если внутри функции не ожидается Future, вы можете вернуть его, не помечая функцию async. Асинхронная функция не только приводит к более сложному коду, созданному компилятором, но также может предотвратить некоторые оптимизации, такие как @pragma(‘vm:prefer-inline’). Эта прагма не работает для функций/методов, которые имеют try блоков, async, async* и sync*, но отлично работает для функций/методов, которые имеют тип возвращаемого значения Future<>.

Итак, правильная версия:

Future<String> parseArgs(Iterable<String> args) {
  final argsWithDefaultCommand = _addDefaultCommand(args);

  return _parse(argsWithDefaultCommand);
}

Future<String> _parse() {
  ...
}

Правило DCM, которое может помочь в обнаружении async случаев использования ключевых слов, которые можно безопасно удалить, называется Avoid-избыточный-асинхронный.

Обратите внимание, что есть несколько допустимых случаев для async без await. Например, если вы возвращаете значение, отличное от Future. Добавление ключевого слова async к такой функции позволяет автоматически заключить значение в Future.

Асинхронная функция для параметра, который ожидает синхронизацию

И последнее, но не менее важное: допустим, у нас есть кнопка Widget с полем VoidCallback.

typedef VoidCallback = void Function();

class RawMaterialButton extends StatelessWidget {
  const RawMaterialButton({
    required this.onPressed,
  });

  final VoidCallback? onPressed;

  void _pressedWrapper() {
    onPressed();

    // Execute some additional logic
  }

  Widget build(BuildContext context) {
    return InkWell(
      onTap: _pressedWrapper(),
    );
  }
}

который используется в другом Widget:

class AnotherWidget extends StatelessWidget {
  Future<void> _someFuture() {
    ...
  }

  Widget build(BuildContext context) {
    return RawMaterialButton(
      onPressed: () async {
        await _someFuture();

        // Execute some additional logic
      },
    );
  }
}

Вы видите здесь проблему?

Ну, так как обратный вызов, переданный в onPressed, возвращает Future<void> (что полностью приемлемо для системы типов Dart), но ожидается тип void, а обратный вызов не ожидается в методе _pressedWrapper, вы можете попасть в состояние гонки, когда _pressedWrapper завершится до того, как обратный вызов передан на onPressed. Это может привести к очень хитрым ошибкам, таким как обратный вызов, выполняемый несколько раз, если пользователь нажимает слишком быстро и т. д.

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

Правильное использование существующего кода

С богатой стандартной библиотекой и количеством доступных виджетов иногда вам все же нужно использовать некоторые замены, сделанные вами. Правило DCM под названием [banned-usage](https://dcm.dev/docs/teams/rules/common/banned-usage/) может помочь гарантировать, что используется правильная версия. Возможности этого правила позволяют вам запрещать как объекты верхнего уровня (например, классы, перечисления, функции), так и определенные методы класса (например, метод сортировки списка).

Например,

void main() {
  final list = [1, 2, 3];
  list.sort();
}

проблема с методом sort заключается в том, что он видоизменяет исходный список, и вы, возможно, хотели бы избежать такого поведения.

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

dart_code_metrics:
  ...
  rules:
    ...
    - ban-name:
        entries:
        - type: List
          entries:
            - ident: sort
              description: Use "sorted" instead

это выделит все вызовы списка sort и предложит использовать sorted (который исходит из пакета коллекции) и не изменяет исходный список.

Другой вариант использования этого правила — запрет стандартных виджетов.

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

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

Вот пример конфигурации для запрещенного использования для достижения этого:

dart_code_metrics:
  ...
  rules:
    ...
    - ban-name:
        entries:
        - ident: TextButton
          description: "Please use SpecialButton in this package."

Это не только выделит все варианты использования TextButton, но и даст указания, что следует изменить.

Читабельность

Учитывая, что мы как разработчики пишем код не только для компилятора, но и для других разработчиков (и обычно вы ставите разработчиков выше компилятора). Вот почему удобочитаемость (особенно в долгосрочной перспективе) так сильно влияет на ремонтопригодность. DCM может помочь и в этом, поэтому вот несколько случаев, которые следует принять во внимание.

Запятые в конце

Завершающая запятая — это запятая, помещаемая в конце списка узлов (например, списка параметров).

Запятые играют особую роль в экосистеме Dart из-за того, как работает dart_style (или dart format). В зависимости от наличия завершающей запятой список будет отформатирован по-разному.

Например,

void thirdFunction(String someLongVarName, void Function() someLongCallbackName,
    String arg3) {
  ...
}

так как после arg3 нет запятой, средство форматирования пытается уместить все аргументы в одну строку.

Но если мы добавим запятую, вот как изменится форматирование:

void thirdFunction(
  String someLongVarName,
  void Function() someLongCallbackName,
  String arg3,
) {
  ...
}

Кому-то второй вариант покажется более читаемым. Чтобы обеспечить наличие завершающих запятых, используйте правило prefer-tailing-comma.

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

void thirdFunction(
  String someLongVarName,
) {
  ...
}

то ли без запятой умещается в одну строку:

void thirdFunction(String someLongVarName) {
  ...
}

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

Обратные вызовы в дереве виджетов

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

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

Давайте посмотрим на этот пример:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: ...,
      onPressed: () {
        // Some
        // Huge
        // Callback
      },
      child: ...
    );
  }
}

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

class MyWidget extends StatelessWidget {
  void _handlePressed() {
    ...
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: ...,
      onPressed: _handlePressed,
      child: ...
    );
  }
}

Правило, которое поможет вам поддерживать эту практику, называется prefer-extracting-callbacks.

Сохранение одного виджета в файле

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

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

Уменьшение вложенности кода

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

Есть несколько подходов, которые вы можете выбрать, чтобы уменьшить уровень вложенности. Один из них — досрочный возврат из блока кода на основе условия.

Например,

void someFunction() {
  if (value == 2) {
    ...

    if (anotherValue == 3) {
      ...

      if (shouldContinue) {
        // Some
        // Calculations
      }
    }
  }
}

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

Чтобы избежать этого, код можно изменить на:

void someFunction() {
  if (value != 2) {
    return;
  }

  ...

  if (anotherValue != 3) {
    return;
  }

  ...

  if (!shouldContinue) {
    return;
  }

  // Some
  // Calculations
}

инвертирование условий и ранний возврат позволяет нам держать уровень вложенности под контролем. Правило DCM для выделения кода, который можно рефакторить таким образом, называется prefer-early-return.

Другой способ уменьшить уровень вложенности — адресовать блоки else в операторах if. Например,

void withInstance() {
  if (someCondition) {
    // do some work
    
    return;
  } else {
    // do some other work
  }

  ...
}

здесь ветвь «тогда» оператора if имеет return, что означает, что весь код после него не будет выполняться, если условие оценивается как истинное.

Это позволяет провести рефакторинг кода, полностью удалив else и тем самым уменьшив уровень вложенности. Давайте взглянем:

void withInstance() {
  if (someCondition) {
    // do some work
    
    return;
  } 
  // do some other work
  
  ...
}

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

Утечки памяти

Предотвращение утечек памяти важно в том смысле, что отладка и воспроизведение таких проблем может занять значительное количество времени. Вот почему разработчики предпочли бы вообще не вводить их.

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

Но есть еще некоторые распространенные ошибки, которые можно проверить.

Удаление и удаление слушателей

Если ваше приложение использует ChangeNotifiers, ValueListenable, ScrollControllers и другие классы, реализующие класс Listenable, то вы, вероятно, знаете, что вам нужно удалять или удалять слушатели, когда они не нужны.

Чтобы помочь вам никогда не забывать, есть два правила всегда-удалять-слушатель (сосредоточено на Listenable) и удалять-поля (для любого поля состояния виджета типа, у которого есть метод dispose).

Например,

class SomeDisposable implements Disposable {
  @override
  void dispose() {}
}

class _ShinyWidgetState extends State<ShinyWidget> {
  final _someDisposable = SomeDisposable();
  final _anotherDisposable = SomeDisposable();

  void dispose() {
    _someDisposable.dispose();

    super.dispose();
  }
}

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

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

Дополнительный бонус: работа с байтами

Если ваше приложение работает с большим количеством байтов, вы найдете этот раздел полезным.

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

Вы только взгляните на эти ориентиры!

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

void run() {
  List<int> buffer = Uint8List(0);
  for (var i = 0; i < 100; i++) {
    buffer = buffer + chunks[i];
  }
}

следует переписать на

void run() {
  final buffer = BytesBuilder();
  for (var i = 0; i < 100; i++) {
    buffer.add(_chunks[i]);
  }
}

И правило для его соблюдения называется prefer-bytes-builder.

Ошибки по невнимательности

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

Неиспользуемые параметры

Например, Dart не предоставляет способа проверить, есть ли у функции или метода неиспользуемые параметры.

С правилом DCM Avoid-unused-parameters] их становится довольно легко обнаружить:

class SomeClass {
  void method(String value, int counter) {
    print(value);
  }
}

В этом случае параметр counter не используется и может быть удален без каких-либо побочных эффектов.

Сложные условия

Другой тип проблем возникает при рефакторинге сложных условий.

Например, следующий код:

final num = 1;
final anotherNum = 2;

void main() {
  if (num == anotherNum && num == anotherNum) {
    return;
  }
}

имеет проблему с обеими частями двоичного выражения (&&), равными, что означает, что код можно упростить до:

final num = 1;
final anotherNum = 2;

void main() {
  if (num == anotherNum) {
    return;
  }
}

и все равно дают тот же результат.

В DCM есть несколько правил, которые могут помочь вам в сложных условиях. избегайте самосравнения и избегайте равнозначных выражений — вот два из них, на которые вам нужно обратить внимание.

Отсутствующие вызовы

Отсутствие вызова — еще одна проблема, которая может возникнуть из-за рефакторинга.

Например,

class SomeClass {
  final void Function() callback;

  const SomeClass({
    required this.callback,
  });
}

void _handle() {
  ...
}

void main() {
  SomeClass(
    callback: () => _handle, 
  );
}

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

void main() {
  SomeClass(
    callback: () => _handle(), 
  );
}

Чтобы избежать подобных ошибок, используйте Avoid-missed-call.

Копировать с помощью

Именование copyWith методов, которые возвращают обновленную сущность неизменяемого класса, является принятой практикой сообщества, которая хорошо работает. Проблема возникает, когда вы обновляете класс, у которого есть этот метод, и забываете обновить метод соответствующим образом (особенно, если класс является частью общедоступного API вашего пакета). Вместо этого вы можете использовать avoid-incomplete-copy-with и никогда больше не сталкиваться с этой проблемой.

Например,

class Person {
  const Person({
    required this.name,
    required this.surname,
  });

  final String name;
  final String surname;

  Person copyWith({String? name}) {
    return Person(
      name: name ?? this.name,
      surname: surname,
    );
  }
}

здесь у нас есть класс с именем Person с двумя полями. Если вы собираетесь создать копию экземпляра этого класса, вам, вероятно, понадобится возможность изменять оба поля. Но здесь метод copyWith принимает только один параметр, что вынуждает нас вручную создавать экземпляр Person, если мы хотим обновить surname. В этом конкретном классе это звучит не так уж и сложно, но если в классе около 20 полей и хотя бы одно из них невозможно обновить через copyWith, это может быть проблемой, приводящей к большому количеству шаблонного кода.

Затенение

Затенение переменной происходит, когда переменная, объявленная в определенной области (блок решения, метод или внутренний класс), имеет то же имя, что и переменная, объявленная во внешней области.

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

Правило DCM для выделения всех проблем с теневым копированием в коде называется Avoid-Shadowing.

Вот как это выглядит в коде:

class SomeClass {
  final String content;

  ...

  void calculate(String content) {
    ...
  }
}

здесь, если вы пишете код в методе calculate и ссылаетесь на content, вы всегда будете ссылаться на параметры метода, если вы явно не используете this.content.

Суперзаказ звонков

Если у вас когда-либо возникали проблемы с супервызовами initState и dispose, DCM поможет вам и в этом!

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

С помощью правила "правильные супервызовы" вы можете перестать проверять и помнить о правильном порядке вызова super — DCM сделает это за вас!

Например,

class _MyHomePageState<T> extends State<MyHomePage> {
  @override
  void dispose() {
    super.dispose();

    someDisposeWork();
  }
}

этот пример кода может выглядеть нормально, но он содержит проблему неправильного порядка super. Если в someDisposeWork есть какая-то работа, которая высвобождает ресурсы, она может никогда не быть завершена!

Бонусный балл: Equatable

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

class AnotherPerson extends Equatable {
  const AnotherPerson(this.name, this.age);

  final String name;

  final int age;

  @override
  List<Object> get props => [name];
}

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

Если у вас когда-либо была эта проблема, не говорите больше. DCM поможет вам в этом и предоставляет список-все-равные-поля. Вы больше никогда не забудете добавить поле в геттер props!

Заключение

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