Какие архитектурные подходы в Dart (вне контекста Flutter UI) ты знаешь или применял?

Ответ

Dart как язык позволяет строить чистую, модульную архитектуру на уровне бизнес-логики и данных, независимо от Flutter. Вот ключевые подходы, которые я применял:

  • Слоистая архитектура (Layered Architecture): Это основа большинства моих проектов. Я четко разделяю код на слои:

    • Domain (Бизнес-слой): Содержит сущности (User, Product), абстрактные интерфейсы репозиториев (UserRepository) и Use Cases (или Interactors) — классы, инкапсулирующие конкретные бизнес-правила. Этот слой не зависит ни от чего внешнего.
    • Data (Слой данных): Реализует интерфейсы из Domain. Содержит конкретные реализации репозиториев, модели данных для API (UserDto), источники данных (локальная БД через sqflite/hive, сетевые клиенты через dio/http).
    • Presentation (Слой представления): Во Flutter это виджеты и state-менеджеры (BLoC, ViewModel), которые используют Use Cases из Domain слоя.
  • Принципы SOLID и Clean Architecture в Dart: Я активно применяю эти принципы для создания поддерживаемого кода. Например, Dependency Injection (внедрение зависимостей) достигается через передачу зависимостей в конструктор, что делает код тестируемым.

    // Domain слой: абстракция
    abstract class AuthRepository {
      Future<User> login(String email, String password);
    }
    
    // Use Case (также Domain слой)
    class LoginUseCase {
      final AuthRepository _repository;
      LoginUseCase(this._repository); // DI через конструктор
    
      Future<User> execute(String email, String password) {
        return _repository.login(email, password);
      }
    }
    
    // Data слой: конкретная реализация
    class AuthRepositoryImpl implements AuthRepository {
      final ApiClient _client;
      AuthRepositoryImpl(this._client);
    
      @override
      Future<User> login(String email, String password) async {
        final dto = await _client.post('/login', data: {'email': email});
        return User.fromDto(dto);
      }
    }
  • Функциональное программирование: Dart поддерживает функции первого класса, Future, Stream и операторы высшего порядка (map, where, fold). Я использую эти возможности для создания чистых функций, обработки асинхронных потоков данных и написания декларативного, предсказуемого кода, особенно в комбинации с async/await.

Такой подход позволяет писать бизнес-логику, которую можно легко тестировать изолированно (юнит-тесты для Use Cases) и повторно использовать, например, в CLI-утилите на Dart, если это потребуется.

Ответ 18+ 🔞

Слушай, я тут подумал — ну реально, Dart же как язык вообще отдельная песня, ёпта! Его можно так отжаренно использовать для архитектуры, что мама не горюй. Главное — не превратить всё в один большой ком говна, где бизнес-логика перемешана с виджетами так, что потом сам от себя охуеешь, пытаясь что-то поменять.

Вот смотри, как я обычно делаю, чтобы не было пиздопроебибны в коде.

Слои, блядь, слои! (Layered Architecture) Это как бутерброд — если всё в одну кучу, будет сосалка. А если по полочкам — красота. Делим всё чётко:

  • Domain (Ядро, бизнес-логика): Тут живут самые главные сущности — User, Product, вся эта хрень. Плюс абстрактные интерфейсы репозиториев (типа UserRepository — обещает, что сможет что-то сделать, но как — не говорит). И ещё Use Cases (или Interactors) — это такие работяги, которые знают конкретные правила: «аутентифицировать пользователя», «оформить заказ». Этот слой — святая святых, он нихуя не знает про Flutter, базы данных или интернет. Он просто знает правила игры.

  • Data (Слой данных): А вот тут уже живут те самые ребята, которые реализуют интерфейсы из Domain. Тут конкретные репозитории, которые уже лезут в sqflite или hive, или дергают API через dio. Тут же всякие DTO-шки — модели данных специально для общения с сервером. Короче, тут вся грязная работа с данными.

  • Presentation (Представление, UI): Ну а это уже Flutter-царство. Виджеты, BLoC'и, ViewModel'и. Их задача — показывать данные юзеру и тыкать в Use Cases из Domain-слоя, когда нужно что-то сделать. Они не должны знать, откуда данные берутся — им похуй, с локальной базы или с сервера.

SOLID и Чистая Архитектура на практике Это не просто умные слова, а реально рабочие штуки, чтобы не выстрелить себе в ногу. Самый главный фокус — внедрение зависимостей (Dependency Injection). Не создавай зависимости внутри класса, а проси, чтобы их передали снаружи. Так код становится тестируемым до овердохуища.

Вот, смотри на примере, как это выглядит в коде. Блоки кода не трогаю, оставляю как есть, но объясню по-человечески.

// Domain слой: абстракция, обещание, контракт.
// Говорим: "Должен быть кто-то, кто умеет логинить".
// Как? Нам похуй на этом уровне.
abstract class AuthRepository {
  Future<User> login(String email, String password);
}

// Use Case (тоже Domain слой). Это уже конкретный кейс "Залогинь пользователя".
// Он ТОЛЬКО бизнес-правило выполняет. Зависимость (репозиторий) ему ПЕРЕДАЮТ в конструктор.
// Это и есть DI — не "создай сам", а "возьми готовое".
class LoginUseCase {
  final AuthRepository _repository;
  LoginUseCase(this._repository); // Внедрили зависимость, красота!

  Future<User> execute(String email, String password) {
    // Правило простое: скажи репозиторию сделать работу.
    return _repository.login(email, password);
  }
}

// Data слой: конкретная реализация того самого обещания.
// Вот этот чувак уже знает про API, Dio, JSONы и прочую сетевую магию.
class AuthRepositoryImpl implements AuthRepository {
  final ApiClient _client; // И ему тоже клиент передали, а не создал он его сам.
  AuthRepositoryImpl(this._client);

  @override
  Future<User> login(String email, String password) async {
    // А вот тут уже пошла конкретика: пост-запрос, DTO-шка.
    final dto = await _client.post('/login', data: {'email': email});
    return User.fromDto(dto); // Превращаем сырые данные с сервера в нашу доменную сущность.
  }
}

Ну и про функциональщину (Functional Programming) Dart-то не дурак, поддерживает функции как объекты первого класса, Future/Stream, всякие map, where. Это ж просто золотая жила! Я это использую, чтобы писать чистые, предсказуемые функции без side-эффектов. Обработка списков, трансформация потоков данных через async/await — это делает код декларативным и, блядь, красивым. Не надо городить циклы и костыли, когда можно элегантно цепочкой операторов всё сделать.

Итог-то какой? А такой, что если так делать, то твою бизнес-логику можно тестировать в полной изоляции, мокая все репозитории. И доверия к ней — ебать! А ещё её, эту логику, можно взять и переиспользовать, например, в какой-нибудь консольной утилите на Dart, если вдруг приспичит. Flutter будет только одним из клиентов для этого ядра. Красота же, ёпта!