Сколько раз вам приходилось запускать flutter create, затем удалять старый добрый «Counter App», добавлять правила линта в анализатор и настраивать структуру папок и файлов? Смею предположить, что это происходит довольно часто. А теперь представьте себе компанию с десятками коммерческих проектов и сотнями внутренних: стало страшно, не правда ли? Старт нового проекта для Flutter-разработчика — это неоптимизируемый процесс.
Если с проблемой инициализации вы не сталкивались, то, я точно уверен, у вас возникало желание сделать свой инструмент или, например, какой-нибудь pet-проект. Как вообще стоит подойти к разработке, какие подходы использовать и какие этапы необходимо пройти?
Меня зовут Иван Таранов, я Flutter-разработчик в Surf. На примере нашего стартера расскажу, как сделать свой инструмент правильно. Советы в определённой мере универсальны для любого проекта.
Проектирование консольной утилиты
Цель — разработать консольную утилиту, которая будет взаимодействовать с пользователем и внешнем слоем — Git-репозиторием. Ключевое требование — максимальная устойчивость к изменениям. Этим изменениям необходимо следовать и максимально безболезненно обновлять нашу «CLI-тулзу».
Задача по созданию проекта может показаться весьма тривиальной со стороны. Решение в виде скрипта уместится в один файл: зачем усложнять? На самом деле «скрипт» не продержался бы и несколько проектов: такой подход не эффективен при создании сложных, долгоиграющих систем.
Соответствие SOLID-принципам и чистой архитектуре позволяет создать не просто скрипт, а рабочий инструмент, который можно развивать и обновлять. Теперь давайте остановимся и разберём основные понятия, чтобы дальше общаться на одном языке.
SOLID
SOLID — аббревиатура пяти основных принципов проектирования в объектно-ориентированном программировании:
-
single responsibility — принципы единственной ответственности,
-
open-closed — открытости и закрытости,
-
Liskov substitution — подстановки Барбары Лисков,
-
interface segregation — разделения интерфейса,
-
dependency inversion — инверсии зависимостей.
Это означает, что все элементы программы:
-
Должны обладать единой ответственностью: один класс не может отвечать за принципиально разные вещи.
-
Быть открытыми для расширения, но не изменения.
-
Соблюдать иерархию: высшие уровни могут зависеть от низших, но не наоборот.
-
Иметь раздельные интерфейсы без строгой зависимости на частную реализацию.
Чистая архитектура
Чистая архитектура — понятие более высокоуровневое: его применяют на стратегическом уровне при составлении архитектуры проекта. Подразумевает независимость от фреймворков, интерфейсов и любых внешних агентов. Это значит, что программа должна быть разделена на логические, не тесно связанные слои: это позволяет изменять и дорабатывать ПО, не нарушая его работоспособность.
Совмещая эти парадигмы, можно максимально избавиться от ранних ошибок на этапе проектирования: паттерны выступают в роли проторенной дорожки. Можно сказать, это рецепт по предотвращению проблем при разработке: то, что позволит не наступать на грабли предшественников.
Взаимодействие и связи инструмента
Давайте схематично отобразим, с чем и посредством каких связей инструмент будет взаимодействовать.

Спецификации
Или же конфиг. В конфигурации задаём кастомные параметры, необходимые для проекта. Происходить это может посредством заполнения файла или в моменте интерактивного взаимодействия CLI и пользователя.
CLI-тулза
Запуск инструмента происходит, когда Flutter-разработчик создаёт проект. Главная задача скрипта — сбор конфигурации и создание проекта. «Скелет», благодаря которому происходит генерация, — это репозиторий на GitHub.
Шаблон проекта
Единый стандарт для проектов. Можно сказать, он служит точкой истины для хранения лучших практик, правил анализатора и пакетов, которые используем в разработке.
Архитектура: связи между классами и интерфейсами
Проект в мире ООП и чистой архитектуры — это сложная система связей и зависимостей от множества групп элементов и параметров. Держать в голове всю эту конструкцию — занятие неблагодарное и, по большей части, бесполезное. Лучше всего проектирование начинать с составления диаграммы зависимостей. Это схема, которая подразумевает определение связей между классами и интерфейсами внутри программы.

С помощью легенды можно установить элементы связи из UML, которые мы используем: наследование, имплементация, наличие и передача. Из сущностей есть два типа элементов: классы и интерфейсы.
Зачем это всё нужно? Ответ прост: при проектировании любого инструмента очень важно выявлять и устранять «красные флаги». Это могут быть перегруженные «God-классы», неявные зависимости, нарушение иерархии слоёв и многое другое. Чем раньше обнаружим проблему, тем быстрее сможем исправить и уменьшить общую стоимость ошибки.
Диаграмма зависимостей отлично подходит для этой задачи: на ней явно будет выявлено, как выглядит архитектура и нет ли на этапе проектирования костылей.

Процессы «внутри»
Диаграмма зависимостей может показать связь между слоями, но не содержание. Для понимания процессов, которые протекают внутри инструмента, понадобится Swimlane-диаграмма.
Swimlane используют в технологических схемах, которые описывают, что или кто работает на определённой части процесса. «Плавательные дорожки» расположены либо по горизонтали, либо по вертикали и используются для группировки процессов или задач в соответствии с обязанностями этих ресурсов, ролей или отделов.
В нашем примере идёт разделение по порядку вызовов и тому, как устроены уровни абстракции в архитектуре. Поэтому получается разделение сверху вниз:
Command > Creator > Job > Repository > Service
На легенде видим, как выглядит стандартный блок диаграммы:

Так выглядит Swimlane нашего алгоритма:

-
Сверху поступают элементы управления: аргументы и параметры.
-
Слева — точка входа: как мы оказались в этом месте.
-
Справа — вызовы: куда переходим.
-
Снизу — точка выхода, то есть артефакты, которые могли создаться из этого процесса.
После создания диаграммы становится ясен путь, по которому «идёт» алгоритм. Это не просто связи в диаграмме зависимостей, а чётко сформулированный процесс с разделением на этапы и определением, какой слой чем занят. Подробнее всего это расписано у Job — «рабочих лошадок» нашей «тулзы». Здесь мы можем видеть, как сначала определяется конфиг проекта, затем создаётся архив, как он распаковывается, переименовывается содержание и получается проект.
Разработка
Основная часть работы заключается в том, чтобы ещё до начала разработки ответить на вопросы, которые возникают во время неё. Если этого не сделать, стоимость ошибок возрастёт. Если устранить максимум возможной неопределённости заранее, разрабатывать и поддерживать проект будет легче .
Config
/// Describes a new project that is being created. /// /// Consists of values & parameters, that are being inserted /// into a new project when it's being created by the user. User /// defines those values & parameters as [ConfigParameter]s /// whilst interacting with CLI. class Config { /* ... */ }
Config — базовый класс, который декларирует описание настроек нового приложения. Config заполняется уникальными полями, которые будут использоваться в настройке и инициализации проекта.
В этом объекте все значимые поля объявлены через Config Parameter — другой объект, в котором хранится простейшее значение определённого параметра и логика его валидации. По сути он чем-то напоминает Value Object из парадигмы Domain Driven Design (DDD): это позволяет получить больший контроль над значениями, хранящимися в конфиге.
/// Directory, in which a new project is created. final ProjectPath projectPath; /// Name of new project. /// /// See also: /// * https://dart.dev/tools/pub/pubspec#name final ProjectName projectName; /// Application Label (name). /// /// See also: /// * https://developer.android.com/guide/topics/manifest/manifest-intro#iconlabel final AppLabel appLabel; /// Application ID. /// /// See also: /// * https://developer.android.com/studio/build/configure-app-module#set_the_application_id final AppID appID;
ConfigBuilder
/// Builds [Config]. /// /// As a whole, it is based on a builder-pattern. It functions as an easier /// method of building [Config] objects, adding its [ConfigParameter]s /// on the way. abstract class ConfigBuilder { /// [Config] private instance. /// /// Default to an empty config with empty parameters. Config _config = Config.empty(); /* Builder methods */ /// Returns [Config] instance. Config build() => _config; /// Builds [Config] with given parameters. Config buildWithParameters({ required String projectPathValue, required String projectNameValue, required String appLabelValue, required String appIDValue, }) { buildProjectPath(projectPathValue); buildProjectName(projectNameValue); buildAppLabel(appLabelValue); buildAppID(appIDValue); return build(); } }
Строитель (builder) — порождающий паттерн проектирования: действует пошагово и упрощает создание объектов. Бывает полезен для инициализации сложных объектов со множеством полей и параметров.
Config Builder реализует этот паттерн и облегчает создание экземпляра Config. Интерфейс содержит ряд билдер-методов, каждый отвечает за инициализацию определённого Config Parameter (скрыто за комментарием). Помимо этого, внутри класса содержится инстанс конфига, который по умолчанию содержит пустые параметры.
MinimalConfigBuilder
/// '[Config]'-MVP like builder, used for initial [Creator.start]. /// /// Consists of: /// [ProjectName], /// [ProjectPath], /// [AppLabel], /// [AppID]. /// /// Is bare minimal of a project entity & its builder only used for /// quick & easy [Creator.start]. class MinimalConfigBuilder extends ConfigBuilder { ... }
Minimal Config Builder реализует интерфейс строителя, переопределяя билдер-методы. Задача конкретно этого строителя: создать простой Config в стиле MVP — то есть с минимальным необходимым набором параметров. Пока что этот билдер единственный в проекте, однако по мере развития возможно будет создать и новых «строителей».
Creator
/// Interface for Project creation. abstract class Creator { /// Main [Creator] entry-point. Future<void> start() async { final config = await prepareConfig(); return createByConfig(config); } /// Retrieves [Config] from somewhere. @protected Future<Config> prepareConfig(); /// Creates Project by given [Config]. /// /// Runs series of [Job]s to do so. @protected Future<void> createByConfig(Config config); }
Creator — «создатель» проекта. В нём описан алгоритм операций, приводящих проект из исходного шаблонного вида в специфический. Более того, Creator играет роль точки входа в систему: отвечает за то, как мы запустили CLI-утилиту. Это может быть как «Interactive CLI creator» для интерактивного взаимодействия с пользователем по ходу создания проекта, так и «Automatic Creator» для автоматической генерации по готовому config-файлу.
При проектировании мы решили использовать паттерн «стратегия». Благодаря нему у утилиты появилась возможность запускать различные Creator из единой точки входа. Это значит, что Creator может реализовать разные сценарии поведения, сохраняя при этом единый интерфейс. В реальности это очень полезное свойство, которое отвечает LSP (Liskov Substitution Principle) из SOLID и упрощает работу с кодовой базой в дальнейшем.
Job
/// Atomic task, which does something and returns `Object?` on completion. /// /// [Job]'s are used for the project generation process. They are top-level entities, /// which define several technical steps of creating a new project. [Job]'s are /// expandable. Meaning, that series of more [Job]'s can create more complex /// structure. abstract class Job { /// Executes specific task for project template creation. /// /// Returns `Object?` Future<Object?> execute(); }
Job — атомарная задача, которая выполняет определённое действие. Job, например, может отвечать за скачивание архива проекта или переименование его составляющих файлов. Удобство применения Job заключается в том, что при изменении бизнес-требований к шаблону проекта, можно с лёгкостью подкорректировать одну из задач, поменять выполнение местами или добавить что-то совершенно новое, не создавая конфликтов с предыдущими Job.
/// [Job] requires [Config], as a project-describing entity. abstract class ConfigurableJob extends Job { /// Instance of [Config]. /// /// Holds [Job]-specific instance of [Config], required for /// [Job.execute] & project creation process. late final Config config; /// Sets up [Job] before its' [Job.execute]. /// /// Requires [Config]. void setupJob(Config config) { this.config = config; } }
Помимо обычного Job, мы активно используем его подвид — Configurable Job. Суть объекта не меняется, так как он всё ещё ответственен за выполнение атомарной задачи, однако теперь для её реализации ему необходим Config. С помощью паттерна Object Injection и метода «setup job» передаём инстанс, который Job сможет использовать при исполнении.
Можно сказать, что Job в какой-то мере реализован по паттерну «цепочка событий»: чтобы не превращать Creator в God-class и оставлять всю специфику создания проекта под его ответственность, мы выделили серию Job, которые собирают проект планомерно — «по кирпичикам».
Серия «Job»:
-
Собирает конфиг проекта от пользователя.
-
Скачивает архив шаблона.
-
Распаковывает и удаляет архив.
-
Заменяет шаблонные данные на значения из конфига.
Тестирование и внедрение

Теперь осталось протестировать инструмент, выложить в pub.dev, открыть репозиторий и распространить на весь отдел. Важно не забыть написать инструкцию: она поможет при использовании стартера в дальнейшем.
Создание проекта — критически важный момент, так как на моменте инициализации мы и закладываем основные подходы в плане разработки: архитектура, стейт-менеджмент, правила линта. Именно поэтому документирование процессов — это очень важно.
Не стоит начинать создание инструмента с программирования
В заключение хотелось бы дать совет: если появилось желание или потребность сделать собственный инструмент, ни в коем случае не стоит начинать его создание с программирования. Сначала лучше уделить внимание проектированию: обдумать проблему, определить архитектуру и возможные способы реализации.
Часто у неопытных разработчиков можно наблюдать подход «нам бы побыстрее закодить». Он приводит к некачественным неподдерживаемым решениям, которые не могут пройти проверку временем. Попробуйте составить примерный план имплементации, нарисовать пару диаграмм: это упростит для вас как процесс создания проекта, так и его использование в дальнейшем.
ссылка на оригинал статьи https://habr.com/ru/company/surfstudio/blog/680480/
Добавить комментарий