DevGang
Авторизоваться

Применение архитектуры в приложениях Flutter

Когда мы начинаем писать приложение Flutter (или любой другой язык/фреймворк в этом отношении), мы всегда имеем в виду архитектуру, которую мы хотим, чтобы наш код уважал и следовал ей. Разные архитектурные стили преследуют разные цели, и каждый из них дает вам разные возможности.

Однако, когда проект становится больше и задействовано больше программистов, код может сбиться с пути, и мы начинаем отклоняться от нашей желаемой архитектуры (исключая случай, когда сама архитектура меняется в рамках эволюционного дизайна). Это явление известно как разрыв между моделью и кодом:

Пробел в модельном коде - это идея из книги Джорджа Фэрбенкса “Архитектура программного обеспечения Just enough”. В нем описывается концептуальный разрыв между абстракциями, которые мы используем для обсуждения архитектуры программного обеспечения (модели), и реальностью выполняемого исходного кода. — Из блога model code gap|IcePanel

Чтобы устранить разрыв между моделью и кодом, у нас есть несколько инструментов и техник, которые мы можем использовать для “принудительного применения архитектуры” (мы рекомендуем прочитать статью, поскольку в ней подробно рассказывается о принудительном применении архитектуры). Эти инструменты можно обобщить от более быстрых и ограниченных до более медленных и гибких следующим образом:

  1. Принудительное выполнение во время компиляции: Эта категория содержит все, что у нас есть в языке, что позволяет нам применять правила и указания по инкапсуляции, упаковке и зависимостям. (например: частные и общедоступные поля, зависимости пакетов и т.д.).
  2. Автоматические проверки: Это включает в себя дополнительную конфигурацию или дополнительный код, который мы используем специально для того, чтобы разрешить / запретить выполнение определенных действий в нашей кодовой базе. Техника статического анализа кода относится к этой категории, и в некоторых экосистемах мы можем писать тесты для архитектуры кода с помощью таких инструментов, как ArchUnitNET
  3. Проверка кода: Это включает в себя человеческий фактор, либо при парном программировании, либо при асинхронных проверках кода, чтобы “вручную” проверить, соблюдается ли архитектура и соответствует ли код желаемому дизайну, который, вероятно, был задокументирован в отчете об архитектурном решении.

Если отбросить теорию, когда дело доходит до кода, архитектура - это не прямоугольники и не стрелки. Это то, какие части кода находятся вместе (сплоченность) и кто это вызывает (сцепление). Итак, применение архитектуры сводится к размещению кода в нужном месте и разрешению его вызова оттуда, где мы хотим, чтобы он был вызван. И в этой статье мы попытаемся рассмотреть то, что есть в наших руках как разработчиков flutter, чтобы достичь этого.

Пример Flutter

Глядя на экосистему Flutter, большинство из нас склонны просто создавать проект и начинать с того, что помещаем все в lib/ directory, не слишком заботясь о границах нашего приложения, даже если мы имели в виду конкретную архитектуру (как здесь и здесь), мы склонны думать об этом в терминах папок и файлов, которые на самом деле вообще не имеют принудительного исполнения, и добавление зависимости из папки в другую - это всего лишь вопрос доступа к действиям IDE и автоматического импорта отсутствующего файла, который добавляет знаменитый импорт ‘file.dart’ в начале файла.

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

Набор инструментов:

Flutter, или, более конкретно, dart, имеет множество механизмов, которые мы можем использовать для обеспечения соблюдения нашей архитектуры и применения одного из вышеупомянутых методов. Мы также постараемся рассмотреть плюсы/минусы каждого из них, чтобы мы могли выбрать подходящий для наших нужд.

Ради демонстрации и простоты мы создадим простое/примерное приложение для запроса вопроса из Trivia API.

Случай с одним файлом

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

Вы можете найти пример здесь.

В нашем случае у нас есть QuizzService, который представляет собой сервис, который будет содержать различные варианты использования, на данный момент он содержит только один вариант использования, “Получить случайный вопрос”. Единственные части, которые нам нужны общедоступными, - это сервис и тип викторины. Все остальные (например, интерфейс репозитория и реализации) являются частными.

Плюсы:

  1. Разрешить строгий контроль общедоступного API: доступны только служба и модель предметной области.
  2. Сплоченность очень высока: поскольку мы собрали все, что связано с функциональностью, в один файл, у нас есть все вместе (но это одновременно и благословение, и проклятие).
  3. Простая структура: использование файлов упрощает навигацию по проекту, а структура папок очень плоская.
  4. Соответствует пакету (файлу, в данном случае) по функциональному дизайну.

Минусы:

  • Ремонтопригодность /удобочитаемость: Наличие всего в одном файле может затруднить работу с различными частями кода и может ухудшить удобочитаемость кода. Особенно, если файл становится больше, а его части (например, база данных или HTTP-вызовы) становятся сложными или очень низкоуровневыми. Это можно решить, имея несколько файлов с функциями dart “library/part/part of”, которые позволят нам создавать несколько файлов, логически эквивалентных одному и тому же файлу, но команда dart не одобряет эти механизмы, и они в любом случае плохо документированы.
  • Односторонняя защита: Хотя мы защитили пользовательский интерфейс от прямого использования репозитория, у нас нет такой защиты наоборот. Таким образом, наш отдельный файл по-прежнему может импортировать виджет (или любого другого типа) и ссылаться на него без каких-либо предупреждений/ошибок.
  • Жесткий:

Для тестирования: трудно выполнить простой модульный тест без загрязнения кода (_FakeRepository и testCompose()) И это побочный продукт нашей жесткой границы. Мы можем настаивать на внедрении зависимостей, но это противоречит цели хранения всего в одном файле, поскольку мы нарушим жесткие границы, которые мы установили с помощью механизма конфиденциальности. Но если мы можем позволить себе использовать только интеграционные тесты, то, возможно, это и прекрасно. Мы попытались использовать ключевые слова ‘library /part/part of’, но их функциональность, похоже, не распространяется на тестовую папку — не могли сказать наверняка, так как не смогли найти никакой официальной документации.

Для расширяемости и возможности повторного использования: Если у нас есть код или логика, которыми мы хотим поделиться между функциями, то мы обязаны поместить все в один файл, иначе создадим дублирование — логика не является частью общедоступного API.

Мы не слишком одобряем это решение из-за различных вышеупомянутых минусов, и нам захотелось сразиться с ним, чтобы протестировать его. и поскольку занимаясь TDD, иногда нужна возможность включать и протестировать логику в сущностях, особенно если это сложная логика (или логика, которая может создать комбинаторный взрыв, если протестирована из клиентского класса/кода и т.д.). Но это наше мнение, и у разных людей разные взгляды, и всегда есть компромисс между детализацией и тестируемостью.

Пакет и анализ

Этот подход использует использование пакетов dart (или flutter) и статического анализатора dart. И, как мы вскоре увидим, это может быть либо автоматическая проверка, либо проверка во время компиляции.

Разница между пакетами dart и flutter заключается в том, что последний зависит от flutter SDK, а первый зависит только от dart SDK. Мы склонны использовать пакеты dart всякий раз, когда у нас есть пакеты, которым не нужны какие-либо элементы пользовательского интерфейса или типы и которые сохраняют зависимость от Flutter только в пакете пользовательского интерфейса (обычно основного проекта).

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

В нашем примере мы создали пакет под названием trivia_quiz, который содержит логику и все типы, связанные с получением случайного вопроса из Trivia API. Пакет также содержит соответствующие модульные и контрактные тесты.

Вы можете найти пример здесь.

Наряду с организацией пакета мы можем использовать анализатор дротика для обеспечения соблюдения стиля импорта и разрешать только операторы импорта, ссылающиеся на пакет, а не на отдельные файлы внутри пакетов с относительным импортом файлов. Как вы можете видеть в analysis_options.yaml, мы пометили две подсказки анализатора как ошибки, таким образом у нас будет более сильное оповещение, когда они произойдут. Нам также нужно было включить always_use_package_imports, так как он не включен в импортированном файле flutter.yaml. в отличие от Avoid_relative_lib_imports.

include: package:flutter_lints/flutter.yaml

analyzer: 
  errors: 
    avoid_relative_lib_imports: error
    always_use_package_imports: error

linter:
  rules:
    - always_use_package_imports

Теперь, если у нас есть какая-либо IDE с поддержкой dart или мы запускаем flutter analyze, мы можем увидеть ошибки, когда пытаемся импортировать что-то, используя относительный путь, минуя общедоступный API пакета. (документация очень хорошо объясняет, как работают пакеты).

Ошибка при попытке относительного импорта файла внутри папки src пакета.
Ошибка при попытке относительного импорта файла внутри папки src пакета.

Также возможно интегрировать flutter analyze в ваш конвейер CI, чтобы предотвратить фиксацию/PR от нарушения вышеупомянутых правил.

Размещение созданных файлов

Поведение этого подхода зависит от того, куда вы помещаете создаваемые вами пакеты:

  1. Внутри папки lib flutter: Это вызовет ошибки linter, упомянутые ранее (случай с предыдущим скриншотом), но не заставит вас импортировать пакет в pubspec.yml flutter.
  2. Вне папки lib flutter: это вызовет упомянутые ошибки linter, а также ошибку компиляции, и проект вообще не будет компилироваться, так что у вас есть гораздо более надежная защита от импорта того, что вы не хотите импортировать. И на любой пакет, который вам нужно использовать, должна быть ссылка в pubspec.yaml ссылающегося пакета. Вызванная ошибка заключается в следующем:
Ошибка компиляции при попытке получить доступ относительно к файлу пакета
Ошибка компиляции при попытке получить доступ относительно к файлу пакета

Плюсы:

  1. Разрешить контроль над общедоступным API: у нас может быть столько файлов, сколько мы хотим в пакете, но мы можем выбрать, какие из них мы хотим, чтобы другие пакеты зависели от использования механизма export и show.
  2. Контролируйте зависимости пакетов: Поскольку каждый пакет имеет свой собственный pubspec.yaml, мы можем явно контролировать, от чего зависит каждый пакет, и выяснить, что использует пакет, довольно просто.
  3. Проще поддерживать и расширять: Наличие полного пакета с исходным кодом и тестом вместе делает его приятнее и проще в обслуживании. И мы не сильно беспокоимся о том, что поместить в публичное и частное (по крайней мере, внутри пакета).
  4. Естественность: по сравнению с решением с одним файлом более естественно работать с пакетами для обеспечения соблюдения требований.

Минусы:

  1. Слишком много папок, слишком много файлов: количество пакетов может создать сложную структуру папок, что может затруднить работу над проектом, особенно при работе с функциями, охватывающими несколько пакетов (но это зависит от того, как мы структурируем наши пакеты).
  2. Полагаться только на ошибки линтера: ошибки линтера — это поддельные ошибки, они не препятствуют компиляции приложения, поэтому для реализации этого потребуется CI, который запускает анализатор и контролирует коммиты, которые нарушают правила линтера. Но это зависит от того, выберете ли вы варианты подходов 1 или 2 указанных выше.

Мы предпочитаем это варианту с одним файлом, и это наш текущий способ работы, как только проект начинает набирать большую кодовую базу и команда начинает расти. (действительно, когда это необходимо).

Внешние пакеты

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

  • import_lint: это может быть очень практично в нашем случае использования, поскольку оно предоставляет правила lint для анализа импорта в кодовой базе. Из README:
analyzer:
    plugins:
        - import_lint

import_lint:
    rules:
        use_case_rule:
            target_file_path: "use_case/*_use_case.dart"
            not_allow_imports: ["use_case/*_use_case.dart"]
            exclude_imports: ["use_case/base_use_case.dart"]
        repository_rule:
            target_file_path: "repository/*_repository.dart"
            not_allow_imports:
                [
                    "use_case/*_use_case.dart",
                    "repository/*_repository.dart",
                    "space\ test/*.dart",
                    "repository/sub/**/*.dart",
                ]
            exclude_imports: []
        domain_rule:
            target_file_path: "domain/**/*_entity.dart"
            not_allow_imports: ["domain/*_entity.dart"]
            exclude_imports: ["domain/base_entity.dart"]
        package_rule:
            target_file_path: "**/*.dart"
            not_allow_imports: ["package:import_lint/import_lint.dart"]
            exclude_imports: []
        core_package_rule:
            target_file_path: "package:core/**/*.dart"
            not_allow_imports: ["package:module/**/*.dart"]
            exclude_imports: []

Это указывает, какой импорт не разрешен, not_allow_imports для файлов, включенных в целевой путь target_file_path.

Этот пакет наследует то, что мы говорили об использовании анализатора ранее, но он предлагает более детальный контроль над зависимостями внутри нашей кодовой базы. Пакет выглядит ухоженным, и его популярность выросла на 85%.

  • dart_code_metrics: Хотя это общий пакет статического анализа, он предоставляет правило, которое может быть нам полезно, избегать запрещенного импорта, и вы можете определить запрещенный импорт следующим образом:
dart_code_metrics:
  rules:
    - avoid-banned-imports:
        entries:
        - paths: ["some/folder/.*\.dart", "another/folder/.*\.dart"]
          deny: ["package:flutter/material.dart"]
          message: "Do not import Flutter Material Design library, we should not depend on it!"
        - paths: ["core/.*\.dart"]
          deny: ["package:flutter_bloc/flutter_bloc.dart"]
          message: 'State management should be not used inside "core" folder.'

Этот пакет также очень популярен и поддерживается, и у него есть другие способы использования, помимо запрета на импорт. Это может быть полезно, если вы уже используете этот пакет в своих проектах и не хотите зависеть от другого пакета статического анализа.

  • dart_arch_test: Этот пакет предоставляет набор инструментов тестирования, который позволяет вам создавать автоматизированные тесты для определения правил зависимостей в вашей кодовой базе.
import 'package:my_package/main.dart';
import 'package:arch_test/arch_test.dart';

void main() {
  archTest(classes.that
    .areInsideFolder('entity')
    .should
    .extendClass<BaseEntity>(),
  );
}

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

В отличие от первого пакета, этот не выглядит популярным и ориентирован только на Linux, macOS и Windows (судя по тому, что мы видим в pub.dev), и это действительно облом для разработчика flutter. И, глядя на репозиторий, видим, что в некоторых филиалах ведется работа, но активности особой нет.

Заключительные слова

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

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

#Flutter
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу