Irbisa · Шпаргалка

Паттерны проектирования

Singleton, Factory, Observer, Builder, Strategy, Command во Flutter

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

  1. 01

    Этот Session задуман как singleton, но токен, сохранённый при логине, на следующем экране равен null. Почему?

    class Session {  Session._();  static Session get instance => Session._();   String? token;} void onLoggedIn(String token) {  Session.instance.token = token;} void onNextScreen() {  print(Session.instance.token); // null}

    instance объявлен как геттер со стрелочным телом, поэтому каждое обращение выполняет Session._() и возвращает новый объект: токен записали в объект, который никто не сохранил.

  2. 02

    Какие паттерны GoF встречаются в самом Flutter и в типичном Flutter-приложении?

    Flutter сам построен на них: дерево виджетов устроено как Composite, ChangeNotifier и Stream реализуют Observer, ScrollPhysics служит Strategy, а Actions/Intent воплощают Command.

  3. 03

    Как реализовать Singleton в Dart и какие у него недостатки?

    Приватный именованный конструктор плюс поле static final с экземпляром; Dart создаёт его лениво при первом обращении, а factory-конструктор может его возвращать, если вызывающим удобнее писать Analytics().

    class Analytics {  Analytics._(); // nobody else can construct it  static final Analytics instance = Analytics._(); // created on first read  factory Analytics() => instance; // optional: Analytics() is the same object   final _events = <String>[];…
  4. 04

    Зачем нужен паттерн Repository и как он позволяет переключаться между локальной базой и удалённым API?

    Репозиторий прячет источник данных за интерфейсом: приложение просит User и не знает, пришёл он из сети, из базы или из кеша.

    abstract interface class WeatherRepository {  Future<Weather> getWeather(String city);} class WeatherRepositoryImpl implements WeatherRepository {  WeatherRepositoryImpl(this._api, this._db);…
  5. 05

    Найдите баг: включение фильтра «Только со скидкой» в корзине уменьшает и итоговую сумму заказа.

    class CartRepository {  final _items = <CartItem>[];   List<CartItem> get items => _items;  int get totalCents =>      _items.fold(0, (sum, i) => sum + i.priceCents * i.qty);   void add(CartItem item) => _items.add(item);} // Cart screen, "On sale only" chipList<CartItem> visibleItems(CartRepository repo, {required bool onSaleOnly}) {  final visible = repo.items;  if (onSaleOnly) visible.removeWhere((i) => !i.onSale);  return visible;}

    Геттер items возвращает собственный список репозитория, а не копию, поэтому removeWhere на экране удаляет товары из самой корзины: фильтр уменьшает totalCents, а скрытые товары пропадают насовсем.

  6. 06

    Как делать внедрение зависимостей во Flutter (get_it, injectable, Riverpod или обычный constructor injection) и в чём их компромиссы?

    Внедрение зависимостей значит, что класс получает зависимости снаружи, а не создаёт их сам, поэтому тест может подсунуть ему фейк.

    // The class itself: constructor injection, no container in sightclass LoginViewModel {  LoginViewModel(this._auth);  final AuthRepository _auth;   Future<void> login(String email, String pass) => _auth.signIn(email, pass);…
  7. 07

    Найдите баг: экран входа работает в первый раз, но при следующем заходе нажатие Sign in бросает StateError.

    void setupLocator() {  sl.registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl(sl()));  sl.registerLazySingleton<LoginBloc>(() => LoginBloc(sl()));} class LoginPage extends StatelessWidget {  const LoginPage({super.key});   @override  Widget build(BuildContext context) => BlocProvider(        create: (_) => sl<LoginBloc>(),        child: const LoginForm(),      );}

    LoginBloc зарегистрирован как lazy singleton, но BlocProvider(create:) владеет созданным экземпляром и закрывает его при уходе со страницы.

  8. 08

    Что такое паттерн Facade и чем он отличается от Repository?

    Facade даёт одну точку входа по форме задачи над несколькими подсистемами: он не хранит данные и не добавляет правил, а лишь знает, как части сочетаются.

  9. 09

    Как применить паттерн Adapter, чтобы обернуть несогласованный сторонний API?

    Adapter оборачивает класс с одним интерфейсом и отдаёт его через тот интерфейс, который нужен вашему приложению.

    // Your contractabstract interface class Analytics {  Future<void> logEvent(String name, Map<String, Object> params);} // Vendor SDK with its own shape (simplified)…
  10. 10

    Как реализовать паттерн Decorator в Dart и когда mixin или extension покрывают ту же задачу?

    Decorator оборачивает объект, добавляя поведение и не меняя его класс: обёртка реализует тот же интерфейс, пробрасывает каждый вызов внутреннему объекту и обкладывает его дополнительной логикой.

    abstract interface class UserRepository {  Future<User> byId(String id);} class LoggingUserRepository implements UserRepository {  LoggingUserRepository(this._inner);…
  11. 11

    Два виджета одновременно вызывают byId('42') через этот кеширующий декоратор. Сколько запросов уйдёт в сеть и как это исправить?

    class CachingUserRepository implements UserRepository {  CachingUserRepository(this._inner);  final UserRepository _inner;  final _cache = <String, User>{};   @override  Future<User> byId(String id) async {    final hit = _cache[id];    if (hit != null) return hit;    final user = await _inner.byId(id);    _cache[id] = user;    return user;  }}

    Два. Оба вызова проверяют кеш до того, как пришёл хоть один ответ, поэтому оба промахиваются; map заполняется только после завершения первого await.

  12. 12

    Функции в Dart являются объектами первого класса, так когда всё же писать интерфейс Strategy вместо колбэка?

    Колбэк уже является стратегией, когда поведение сводится к одной операции без состояния и без идентичности.

    // A plain function is enough: one operation, no statetypedef PriceFormatter = String Function(int cents); // An interface earns its place: several operations, config and a nameabstract interface class RetryPolicy {  String get name; // identity for logs and tests…
  13. 13

    Как реализовать паттерн Command с поддержкой undo/redo?

    Command превращает операцию в объект с методом execute() и, если нужна история, с undo().

    abstract interface class Command {  void execute();  void undo();}class History {  final _undo = <Command>[];…
  14. 14

    Именованные аргументы, каскады и copyWith уже закрывают почти весь GoF-паттерн Builder. Когда в Dart всё ещё стоит писать настоящий класс-билдер?

    Для обычного конструирования практически никогда.

  15. 15

    Что такое паттерн Template Method и как его использует жизненный цикл State во Flutter?

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

  16. 16

    Как паттерн Observer связан с ChangeNotifier, Stream и BLoC?

    ChangeNotifier, ValueNotifier и Stream служат готовыми реализациями Observer: субъект хранит список слушателей и вызывает их при изменении.

  17. 17

    Память растёт каждый раз, когда пользователь открывает и закрывает экран, и все цепочки удержания упираются в глобальный ChangeNotifier. Что пошло не так?

    Каждый экран подписывался через addListener и не вызывал removeListener в dispose, поэтому долгоживущий ChangeNotifier через список слушателей удерживает каждый старый State; замыкание, захватившее this, тянет за собой всё поддерево.

    class _ProfileState extends State<Profile> {  late final AuthNotifier _auth;   @override  void initState() {    super.initState();…
  18. 18

    Как паттерн State соотносится с sealed-классами Dart 3?

    Оба описывают объект, который ведёт себя по-разному в каждом из нескольких режимов.

    // State pattern: behaviour and transitions live in the state objectsabstract interface class PlayerState {  PlayerState onTap();}class Playing implements PlayerState {  @override…
  19. 19

    API ленты помечает каждый элемент полем type, и новые виды появляются каждый спринт. Как это парсить и что делать старой версии приложения с незнакомым типом?

    Диспетчеризуйте по полю type ровно в одной фабрике, а ветку «неизвестный тип» сделайте настоящей моделью, а не броском исключения: приложение, падающее на невиданном типе, чинится только выпуском новой версии.

    sealed class FeedItem {  const FeedItem();   // One factory owns the dispatch; map patterns also check the field types  factory FeedItem.fromJson(Map<String, dynamic> json) => switch (json) {        {'type': 'post', 'text': String text} => PostItem(text),…
  20. 20

    Как собрать композируемую цепочку middleware в Dart и когда это лучше набора if-ов?

    Это Chain of Responsibility: смоделируйте каждую стадию как функцию от запроса и следующей стадии, сверните список в один конвейер; он лучше if-ов, когда важны порядок, набор по окружениям или отдельное тестирование стадий.

    typedef Handler = Future<Response> Function(Request req);typedef Middleware = Handler Function(Handler next); // The first stage in the list becomes the outermost oneHandler pipeline(List<Middleware> stages, Handler endpoint) =>    stages.reversed.fold<Handler>(endpoint, (next, stage) => stage(next));…
  21. 21

    Как выпустить один API с разными реализациями под мобильные платформы и веб, если dart:io в браузере бросает исключение, а package:web не компилируется на мобильных?

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

    // secure_store.dart: the only file the app importsexport 'src/secure_store.dart';export 'src/create_store_stub.dart'    if (dart.library.io) 'src/create_store_io.dart'    if (dart.library.js_interop) 'src/create_store_web.dart';…
  22. 22

    Вы рендерите AST документа в виджеты, и из того же дерева нужен ещё простой текст для поиска: Visitor или sealed-класс с двумя switch?

    В Dart 3 по умолчанию выбирают sealed-switch: он даёт ту же проверку полноты компилятором, что и Visitor, но без шаблонных accept/visit и двойной диспетчеризации.

    sealed class Node {} final class Heading extends Node {  Heading(this.level, this.text);  final int level;  final String text;…