Irbisa · Шпаргалка

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

Принципы SOLID в Dart и Flutter, DRY, KISS и YAGNI, связность и зацепление, закон Деметры, внедрение зависимостей и service locator, code smells и рефакторинг

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

  1. 01

    Что означают DRY, KISS и YAGNI, и когда устранение дублирования делает код хуже?

    DRY: у каждого знания в системе одно место.

  2. 02

    Что такое связанность (coupling) и связность (cohesion), и почему нужны низкая связанность и высокая связность?

    Связанность (coupling) показывает, насколько модуль зависит от деталей другого; связность (cohesion) показывает, насколько тесно связано то, что лежит внутри одного модуля.

  3. 03

    Что такое «запах кода» (code smell), и какие запахи чаще всего встречаются во Flutter-коде?

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

  4. 04

    Что такое рефакторинг, и как не дать рефакторингу изменить поведение?

    Рефакторинг меняет структуру кода, не меняя того, что код делает.

  5. 05

    State экрана оформления заказа разросся до 1000 строк: он валидирует форму, вызывает API, форматирует цены и строит UI. Как применить к нему Single Responsibility?

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

    // Formatting: pure, so the cart and the receipt print prices the same wayclass PriceFormatter {  const PriceFormatter(this.currency);  final String currency;  String format(int cents) => '${(cents / 100).toStringAsFixed(2)} $currency';}…
  6. 06

    Как принцип открытости/закрытости (Open/Closed) выглядит во Flutter для переиспользуемого виджета и для сервиса?

    Открыт для расширения, закрыт для изменения: новый случай приходит новым кодом, подключённым к существующей точке расширения, а не очередной правкой кода, который уже работает.

    // before: a flag for every screen that needed something different// ProductCard(product, showBadge: true, isCompact: false, showFavorite: true) // after: slots, so a screen extends the card without editing itclass ProductCard extends StatelessWidget {  const ProductCard(…
  7. 07

    Какой принцип SOLID нарушает LimitedCart, и почему buyAgain возвращает 5 для заказа из пяти товаров, когда в LimitedCart(3) лежат только 3?

    class Cart {  final _items = <String>[];  int get count => _items.length;  void add(String sku) => _items.add(sku);} class LimitedCart extends Cart {  LimitedCart(this.max);  final int max;  @override  void add(String sku) {    if (count < max) super.add(sku); // full: quietly does nothing  }} // "Buy again" re-adds a past order and reports how many items went inint buyAgain(Cart cart, List<String> skus) {  skus.forEach(cart.add);  return skus.length;}

    Принцип подстановки Лисков (Liskov Substitution).

  8. 08

    Виджет-тесты проходят на фейковом репозитории, а настоящий в продакшене ведёт себя иначе. Как сделать так, чтобы фейк действительно можно было подставить вместо настоящего?

    Считайте фейк ещё одной реализацией, которая обязана соблюдать тот же контракт, и докажите это: опишите контракт один раз общим набором тестов и прогоняйте его и на фейке, и на настоящем репозитории.

    // test/orders_repository_contract.dart: written oncevoid ordersRepositoryContract(Future<OrdersRepository> Function() create) {  late OrdersRepository repo;  setUp(() async => repo = await create());   test('a placed order is returned by the next read', () async {…
  9. 09

    В модели есть PremiumUser extends User, а экраны проверяют user is PremiumUser. Что ломается, когда пользователь покупает подписку посреди сессии, и как это смоделировать?

    Объект не может сменить свой класс, поэтому апгрейд означает замену User, и каждый экран, кэш и провайдер, держащий старый объект, продолжает показывать рекламу.

    enum Feature { noAds, offlineDownloads } // The plan is data that changes; the user stays the same usersealed class Plan {  const Plan(this.features);  final Set<Feature> features;…
  10. 10

    Как принцип разделения интерфейсов (Interface Segregation) работает в Dart, где каждый класс уже является неявным интерфейсом?

    Клиенты не должны зависеть от методов, которые не вызывают.

    // Two roles instead of one fat OrdersServiceabstract interface class OrderHistory {  Future<List<Order>> recent({int limit = 20});} abstract interface class OrderPlacement {…
  11. 11

    Чем отличаются инверсия зависимостей (Dependency Inversion), внедрение зависимостей (DI) и сервис-локатор?

    Инверсия зависимостей описывает правило дизайна: высокоуровневый код владеет абстракцией, а низкоуровневый её реализует, поэтому зависимость направлена к бизнес-логике.

    // Injection without inversion: passed in, but shaped by Dio's APIclass OrdersViewModelA {  OrdersViewModelA(this._api);  final DioOrdersApi _api;  Future<int> count() async => (await _api.getOrders()).length;}…
  12. 12

    Каждый класс в Dart уже является неявным интерфейсом. Когда стоит писать отдельный abstract interface class с единственным Impl, а когда это пустая церемония?

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

    // One concrete class: no ClockService plus ClockServiceImplclass Clock {  const Clock();  DateTime now() => DateTime.now();}…
  13. 13

    Что говорят закон Деметры и принцип «говори, а не спрашивай» (tell, don't ask), и что не так с context.read<Session>().user!.cart.items.add(item)?

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

    // before: the button knows how the session is built and the cart's rulesvoid onAddPressedBefore(BuildContext context, Item item) {  final cart = context.read<Session>().user!.cart;  if (cart.items.length < 10) {    cart.items.add(item);    cart.total += item.priceCents;…
  14. 14

    Найдите баг: экран настроек бросает LateInitializationError, но только когда приложение открыли из push-уведомления.

    class Prefs {  late SharedPreferences _prefs;  Future<void> init() async => _prefs = await SharedPreferences.getInstance();  bool get darkMode => _prefs.getBool('dark_mode') ?? false;} final prefs = Prefs(); // SettingsPage reads prefs.darkMode in build class _HomePageState extends State<HomePage> {  @override  void initState() {    super.initState();    prefs.init();  }   @override  Widget build(BuildContext context) => const FeedView();}

    Prefs работает только после init(), а вызывает его лишь HomePage.

  15. 15

    Какие запахи кода вы видите в этом вызове placeOrder, и какой рефакторинг лечит каждый из них?

    Future<String> placeOrder(String userId, String productId, int quantity,    String street, String city, String zip, double price, String currency,    bool isGift, bool express) async {  return 'order-1'; // creates the order and returns its id} Future<String> onBuyPressed(User user, Product product) =>    placeOrder(user.id, product.id, 1, 'Abay Ave 10', 'Almaty', '050000',        19.99, 'USD', true, false);

    Длинный список параметров, группа данных (data clump), одержимость примитивами и флаговые аргументы.

  16. 16

    Файл utils.dart разросся до 2000 строк, и его импортирует каждая фича. Что с ним не так, и куда должны переехать его функции?

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

  17. 17

    Каждый репозиторий наследует дженерик BaseRepository<T> с пятью CRUD-методами. Через год половина переопределений бросает UnimplementedError. Что пошло не так?

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

    // before: every repository has to pretend it supports everythingabstract class BaseRepository<T> {  Future<List<T>> getAll();  Future<T?> getById(String id);  Future<void> delete(String id); // plus create() and update()}…
  18. 18

    Вы выносите пакет payments, которым пользуются три приложения. Как спроектировать его публичный API, чтобы внутренности можно было менять без мажорного релиза?

    Открывайте наружу минимальную поверхность, которая покрывает сценарии приложений, и прячьте каждое решение, которое может измениться: SDK вендора, HTTP-клиент, хранилище.

    // lib/payments.dart: the only file apps importexport 'src/charge_result.dart' show ChargeResult, Charged, Declined;export 'src/money.dart' show Money;export 'src/payments.dart' show Payments; // lib/src/payments.dart…