Irbisa · Шпаргалка

Архитектурные паттерны

MVC, MVP, MVVM, Clean Architecture, паттерн Repository

Middle-разработчик · 22 вопроса

  1. 01

    Сравните паттерны MVC, MVP, MVVM и MVI и как они ложатся на Flutter.

    Четыре способа отделить UI от логики.

  2. 02

    Объясните Clean Architecture во Flutter: слои и зоны ответственности.

    Clean Architecture делит приложение на три слоя с одним правилом: зависимости направлены только внутрь.

    // domain: pure Dart, no Flutter or package importsclass Product {  const Product({required this.id, required this.name, required this.price});  final int id;  final String name;  final double price;…
  3. 03

    Что рекомендует официальный гайд Flutter по архитектуре и чем он отличается от Clean Architecture?

    Гайд на docs.flutter.dev рекомендует MVVM в два слоя: UI-слой из view и view model и слой данных из репозиториев и сервисов.

  4. 04

    Что значит применять SOLID-принципы во Flutter?

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

  5. 05

    Когда Clean Architecture оказывается неверным выбором для Flutter-приложения и что оставить вместо неё?

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

  6. 06

    Как чисто маппить между доменными entity и data DTO?

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

    // data layer: knows the wire formatclass UserDto {  const UserDto({required this.id, required this.fullName, this.avatarUrl});  final String id;  final String fullName;  final String? avatarUrl;…
  7. 07

    Ревьюер завернул вашу доменную сущность из-за импорта package:flutter/material.dart ради поля Color. Это придирка?

    Не придирка: один этот импорт разворачивает стрелку зависимостей.

    // domain: the meaning, no Flutter importenum OrderStatus { pending, paid, shipped, cancelled } // presentation: the look, chosen where Flutter already livesextension OrderStatusStyle on OrderStatus {  Color color(ColorScheme scheme) => switch (this) {…
  8. 08

    В вашем UserRepository четырнадцать методов, один в один повторяющих REST-роуты, и все возвращают Response. Что вы поменяете?

    Это HTTP-клиент, надевший имя репозитория.

    // before: an HTTP client wearing a repository's nameabstract class UserRepository {  Future<Response> getUserV2(String id);  Future<Response> postAvatarUpload(FormData data);  // ...twelve more, one per route}…
  9. 09

    Все правила о том, когда Order можно отменить, живут в Cubit, а Order остаётся просто набором полей. Чем это обходится?

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

    class Order {  Order({required this.id, required this.status, required this.placedAt, this.paidAt}) {    // invariant: an invalid Order cannot be constructed    if (status == OrderStatus.paid && paidAt == null) {      throw ArgumentError('A paid order needs paidAt');    }…
  10. 10

    QA присылает скриншот: на экране одновременно спиннер и баннер ошибки; состояние экрана хранится в полях isLoading, error и items. Что не так с моделью?

    Три независимых поля (isLoading, error, items) дают восемь комбинаций, а законных у экрана всего четыре.

    sealed class OrdersState {  const OrdersState();}final class OrdersLoading extends OrdersState { const OrdersLoading(); }final class OrdersEmpty extends OrdersState { const OrdersEmpty(); }final class OrdersFailed extends OrdersState {…
  11. 11

    Какие архитектурные правила нарушает этот Cubit и как бы вы его переделали?

    class ProfileCubit extends Cubit<ProfileState> {  ProfileCubit(this.context) : super(ProfileLoading());  final BuildContext context;   Future<void> load() async {    final res = await Dio().get('https://api.example.com/me');    final user = User.fromJson(res.data as Map<String, dynamic>);    emit(ProfileLoaded(user));    if (user.email == null) {      ScaffoldMessenger.of(context)          .showSnackBar(const SnackBar(content: Text('Add your email')));    }  }}

    Он делает работу трёх слоёв сразу: сам создаёт Dio, парсит JSON и держит BuildContext, чтобы показывать UI.

  12. 12

    Придя с Android, вы создаёте ViewModel как в коде ниже: она перезагружается при каждом ребилде и сбрасывается при переключении вкладок. Где во Flutter живёт ViewModel?

    class ProfileTab extends StatelessWidget {  const ProfileTab({super.key});   @override  Widget build(BuildContext context) {    final vm = ProfileViewModel(context.read<UserRepository>())..load();    return ListenableBuilder(      listenable: vm,      builder: (context, _) => Text(vm.name ?? 'Loading'),    );  }}

    ViewModel живёт в том, что владеет ею в дереве виджетов: State или элемент провайдера выше экранов, которые её используют; создаётся один раз и диспоузится вместе с владельцем.

  13. 13

    Вы меняете имя на экране настроек, а в шапке до перезапуска висит старое. Какой слой чинить и как?

    Чинить надо репозиторий, а не экраны: каждый экран загрузил свою копию, единого источника правды нет.

    // BehaviorSubject comes from package:rxdartclass UserRepository {  UserRepository(this._api);  final UserApi _api;  // replays the last value to every late subscriber  final _user = BehaviorSubject<User>();…
  14. 14

    Найдите баг: без сети экран заказов пишет «У вас пока нет заказов» вместо ошибки с кнопкой «Повторить».

    class OrdersRepository {  OrdersRepository(this._api);  final OrdersApi _api;   Future<List<Order>> fetchOrders() async {    try {      final dtos = await _api.getOrders();      return dtos.map((dto) => dto.toEntity()).toList();    } catch (e, st) {      log('fetchOrders failed', error: e, stackTrace: st);      return [];    }  }}

    Репозиторий превращает любую ошибку в пустой список, и Cubit не может отличить «нет заказов» от «нет сети»: он показывает пустое состояние, кнопка «Повторить» не появляется, а ответ 500 или сломанный маппинг выглядят точно так же.

  15. 15

    Как структурировать большое Flutter-приложение (50+ экранов, десять разработчиков), чтобы его можно было поддерживать?

    Группируйте файлы по фичам, а не по слоям: в lib/features/cart/ лежат модели, репозиторий, state holder и экраны корзины, а не разбросаны по lib/models/, lib/blocs/ и lib/screens/.

    lib/  main.dart  app/                    # composition root, router, theme    app.dart    router.dart  core/                   # shared infrastructure, nothing feature-specific…
  16. 16

    Как разбить большое Flutter-приложение на модульные пакеты или workspaces?

    Вынесите каждую фичу в отдельный локальный пакет в packages/, а тонкий пакет app/ пусть зависит от них.

    # pubspec.yaml at the repository root (pub workspaces, Dart 3.6+)name: shop_workspacepublish_to: noneenvironment:  sdk: ^3.6.0workspace:…
  17. 17

    Как настроить dependency injection в масштабе (composition root, scope-ы, окружения)?

    Стройте весь граф объектов в одном месте (composition root, обычно main.dart) и передавайте зависимости вниз через конструкторы.

    // main.dart: the composition root, the only file that names concrete typesvoid main() {  const env = String.fromEnvironment('ENV', defaultValue: 'prod');  // app scope: lives as long as the process  final api = ApiClient(baseUrl: env == 'prod' ? prodUrl : stagingUrl);  final AuthRepository auth = HttpAuthRepository(api);…
  18. 18

    Где по слоям вы ловите, преобразуете и показываете ошибки и когда сбой оформляют как Result, а не как исключение?

    Ловите транспортные исключения один раз, в репозитории, и перебрасывайте их как sealed class AppError; view model кладёт её в состояние, а UI делает по ней switch.

    sealed class AppError implements Exception {  const AppError();}final class NetworkError extends AppError { const NetworkError(); }final class Unauthorized extends AppError { const Unauthorized(); }final class ServerError extends AppError {…
  19. 19

    Как feature flag-и и A/B-тесты вписываются в архитектуру Flutter-приложения?

    Feature flag отделяет деплой от релиза: код уезжает выключенным, а включаете вы его позже (части пользователей или всем сразу).

  20. 20

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

    Не давайте фичам импортировать друг друга: это тот самый цикл, который потом никто не распутает.

    abstract interface class SignOutHandler {  Future<void> onSignedOut();} // Lives in the composition root: the one file that knows every feature.class SessionCoordinator {…
  21. 21

    Оформление заказа резервирует товар, списывает деньги и чистит корзину. Какой слой владеет этой последовательностью и что делать, если списание упало?

    Последовательностью владеет use case: она охватывает три репозитория, ровно для этого и нужен доменный слой.

    class PlaceOrder {  PlaceOrder(this._stock, this._payments, this._cart);  final StockRepository _stock;  final PaymentRepository _payments;  final CartRepository _cart;…
  22. 22

    Вам достался Flutter-проект с логикой в StatefulWidget-ах и синглтонами повсюду. Как улучшить его без переписывания?

    Выдавливайте старый код постепенно (паттерн Strangler), а не переписывайте: зафиксируйте старую форму, все новые фичи делайте в целевой, а старое переносите только тогда, когда и так открыли этот файл.