Какие принципы программирования ты применяешь при разработке на Dart/Flutter?

«Какие принципы программирования ты применяешь при разработке на Dart/Flutter?» — вопрос из категории SOLID и принципы, который задают на 36% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В разработке на Flutter я активно применяю принципы SOLID, DRY и KISS, адаптируя их под специфику фреймворка и языка Dart.

SOLID в контексте Flutter/Dart:

  • Принцип единственной ответственности (SRP): Каждый класс или виджет должен решать одну задачу. Например, я разделяю StatefulWidget на:

    • Виджет-экран, который собирает UI.
    • Отдельный класс-логики (ChangeNotifier, Bloc, Cubit), который управляет состоянием и бизнес-логикой.
    • Сервис-класс для работы с API или базой данных.
  • Принцип открытости/закрытости (OCP): Код должен быть открыт для расширения, но закрыт для изменений. Во Flutter это достигается через композицию виджетов, а не наследование. Вместо того чтобы модифицировать сложный виджет, я создаю новый, который оборачивает или комбинирует существующие.

    // Вместо модификации кнопки, создаю новый виджет-обертку.
    class IconButtonWithLabel extends StatelessWidget {
      final IconData icon;
      final String label;
      final VoidCallback onPressed;
    
      const IconButtonWithLabel({...});
    
      @override
      Widget build(BuildContext context) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(icon: Icon(icon), onPressed: onPressed),
            Text(label),
          ],
        );
      }
    }
  • Принцип подстановки Барбары Лисков (LSP): Подклассы должны быть взаимозаменяемы с родительскими классами. В Dart это означает корректное использование @override и соблюдение контрактов абстрактных классов (например, Widget или State).

  • Принцип разделения интерфейса (ISP): Вместо одного "жирного" абстракного класса с множеством методов лучше создать несколько специфичных интерфейсов (в Dart — abstract class). Например, отдельный интерфейс для локального кэша и отдельный — для сетевого клиента.

  • Принцип инверсии зависимостей (DIP): Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. На практике я использую dependency injection (через get_it, riverpod или конструкторы) и завишу от абстракций (Repository, DataSource), а не от их конкретных реализаций (ApiClient, LocalStorage).

DRY (Don't Repeat Yourself): Повторяющуюся логику (валидация форм, форматирование дат, стили виджетов) я выношу в утилитные классы, extensions или общие виджеты-компоненты.

KISS (Keep It Simple, Stupid): Я стремлюсь к простым и понятным решениям. Например, предпочитаю StatelessWidget и StatefulWidget там, где достаточно, и не усложняю архитектуру (например, не внедряю Bloc для простого экрана с одной кнопкой).