Для чего нужна архитектура?

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

Clean Architecture

Clean Architecture является одной из самых популярных архитектур в мире разработки программного обеспечения, особенно для крупных и средних проектов. Она разработана Робертом Мартином (Uncle Bob) и получила широкое распространение благодаря своей модульности, гибкости и легкости в сопровождении.

Почему же она завоевала такую популярность и симпатию среди Flutter разработчиков и не только? Вот несколько причин, почему эта архитектура столь популярна и эффективна:

Четкое разделение ответственности на слои:

  • Презентационный слой (Presentation): Отвечает за пользовательский интерфейс и взаимодействие с пользователем. Здесь находятся виджеты Flutter, которые отображают данные и обрабатывают пользовательские действия. Кроме интерфейса в текущем слое находится наш менеджер состояния и view модель.

  • Слой домена (Domain): Содержит бизнес-логику и правила приложения. Это сердцевина нашей фичи или приложения, которая независима от фреймворков и внешних библиотек.

  • Слой данных (Data): Отвечает за управление данными, включая работу с базами данных, сетевыми запросами и кешированием.

Позже мы разберем где и какие именно папки, классы и их реализации должны находится.

Какие шаблоны проектирования нужны для чистой архитектуры?

MVVM - Model View View Model - это архитектурный паттерн, который используется для разделения логики представления (UI) и бизнес-логики в приложениях, стоит использовать только в том случае если в нашем виджете , который должен быть максимально “глупым”, существует зависимости от репозитория или же методов. Если присутствует большое количество переменных, переопределенных методов по типу initState так же нужно создать viewModel, в которую мы перенесем всю ненужную "глупому" виджету логику. Ниже нарисованная схема поможет лучше понять смысл этого паттерна.

Inversion of Control (инверсия управления) - это некий абстрактный принцип, набор рекомендаций для написания слабо связанного кода. Суть которого в том, что каждый компонент системы должен быть как можно более изолированным от других, не полагаясь в своей работе на детали конкретной реализации других компонентов.

Dependency Injection (внедрение зависимостей) - это одна из реализаций этого принципа помимо этого есть еще: Service Locator - широко известный анти-шаблон. А чем он занимается? Предоставляет доступ одних объектов к другим объектам.

Принципы ViewModel

В каждом проекте вы неизбежно столкнетесь с использованием ViewModel. Понимание этой концепции часто вызывает затруднения у начинающих разработчиков. Поэтому не будет лишним её разобрать.

Model - Модель представляет данные и бизнес-логику приложения. Она отвечает за управление данными и обеспечение согласованности и целостности приложения. В нашем случае Model будет абстрактным репозиторием из Domain слоя.
View - View отвечает за представление данных пользователю и захват пользовательских взаимодействий. Это пользовательский интерфейс, с которым взаимодействуют пользователи. Сохраняйте виджеты максимально «глупыми», минимизируя логику в компонентах пользовательского интерфейса вынося их в ViewModel.
ViewModel - это посредник между Model и View. Он содержит логику представления, раскрывая данные и команды, к которым View может привязываться. ViewModel разработан для тестирования независимо от UI. Он также часто инкапсулирует состояние View и обрабатывает ввод и взаимодействие пользователя.

Пример ViewModel:

class MapViewModel {
  MapViewModel({required this.context,required this.infoLocationRepository,required this.locationRepository});
  final BuildContext context;
  final LocationRepository locationRepository;
  final InfoLocationRepository infoLocationRepository;
  
  late final UserLocationBloc locationBloc = UserLocationBloc(repository: locationRepository);
  late final UserInfoLocationBloc userInfoLocationBloc = UserInfoLocationBloc(infoLocationRepository: infoLocationRepository);
  late GoogleMapController mapController;
  
  ValueNotifier selectLat = ValueNotifier([double]);
  ValueNotifier selectLng = ValueNotifier([double]);
  String styleMap = '';
  get onMapCreated => _onMapCreated;
  get initialize => _initialize;

  void _onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }

  void _initialize() {
    locationBloc.add(GetStartUserLocation());
    DefaultAssetBundle.of(context).loadString('style.json').then((onValue) {
      styleMap = onValue;
    });
  }
}

Теперь наш виджет по настоящему "глупый", в нем нет реализации методов и прочего, теперь это все в нашей view model.

Структура проекта:

Utils

Utils содержит в себе все настройки и коллекции приложения. Тема, цвета нашего приложения. Коллекции с нашими svg/png картинками. Какая-то часть разработчиков называют данную папку theme.

Features

Features папка будет содержать все функции приложения, такие как аутентификация, профиль… А уже каждая фича будет разделяться на слои:

Data Layer

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

  • Repositories - Здесь хранится вся реализация бизнес логики, которая имплементируется от абстрактных классов описанных в Domain.

  • Models - Модельки наших ответов с сервера.

  • Service - Тут хранится реализация логики обращения к API или же к БД. В моем случае я использую библиотеку Retrofit, которая не нуждается в обработке и долгой реализации, именно поэтому наш абстрактный класс будет реализован сразу в Data. Как вы видите благодаря многим инструментам мы можем отойти от строгости структурирования чистой архитектуры.

Domain Layer

Domain Layer, также известный как Business Logic или Use Case Layer, содержит основные бизнес-правила и логику приложения. Он представляет собой сердце программной системы, инкапсулируя существенную функциональность, которая не зависит от какой-либо конкретной структуры.

  • Repositories - Здесь мы объявляем наши абстрактные классы, которые реализованы в data.

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

  • Use_case - часто реализуются в виде отдельных классов или методов, которые инкапсулируют бизнес-логику. Это позволяет отделить бизнес-логику от деталей реализации (интерфейсы пользователя, базы данных и т.д.) и сделать код более модульным и тестируемым.

Presentation Layer

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

  • Bloc - У вас эта папка может называться по другому , все зависит от вашего стейт-менеджера, у меня это блок.

  • Page - здесь хранятся наша верстка.

  • View Model - Здесь находится модель наших экранов

  • Widget - Виджеты которые дают верстке не читабельный вид и требуют вынесения в отдельные файлы (например из-за своей громоздкости).

Core

Папка core представляет собой фундаментальный модуль, содержащий ключевые компоненты, такие как утилиты, маршруты, сеть, службы, валидаторы и т. д. Часть разработчиков кучу папок суют в core, а кто-то разделяет как и features на слои: domain, data, presentation; как это делаю я и большинство разработчиков. Так легче ориентироваться и находить нужные тебе файлы.

Data:

  • Storage - Описание работы с нашим хранилищем.

  • Translation - наша логика перевода.

Domain:

  • Di - di container в котором мы проинициализируем зависимости приложения (классы которые должны быть single ton).

  • Repository - здесь хранится вся логика наших шаблонов применяемых к большому количеству реализаций. Например Dio Interceptor - настраивает все запросы (В моем случае добавляет в хедеры токен, который используется во всех запросах) или ответы. (У меня при ответе сервера 401 ошибки выполняется перезаписывание токена и повторное обращение к серверу)

  • Router - тут находятся наши пути, по которым мы будем перемещаться между экранами.

  • Use_case - тот же шаблон как и repository но применяемый не для самой реализации, а для обработки существующей реализации. Например шаблон ответа от сервера:

sealed class Result<T> {
  bool get isSuccess;

  const factory Result.data(T data) = DataResult;

  const factory Result.error(List<String> errorList) = ErrorResult;
}

class DataResult<T> implements Result<T> {
  final T data;

  const DataResult(this.data);

  @override
  bool get isSuccess => true;
}

class VoidResult<T> implements DataResult<T?> {
  @override
  bool get isSuccess => true;

  @override
  final T? data;

  const VoidResult([this.data]);
}

class ErrorResult<T> implements Result<T> {
  final List<String> errorList;

  const ErrorResult(this.errorList);

  @override
  bool get isSuccess => false;
}

То есть мы обрабатываем уже ответ от существующей реализации.

Presentation:

  • Page - тут находятся наши базовые скрины, такие как BottomNavigation. Которые управляют навигацией всего приложения.

  • Widget - тут все, часто переиспользуемые виджеты.

У чистой архитектуры нет строгих принципов. Это лишь базовые папки и подходы, которые были описаны в этой статье. В каждой компании свои подходы, но познакомившись с той структурой, что описал я, не будет никаких проблем разобраться в каком либо уже существующем проекте. Я вам гарантирую, что в core/domain не будут только те папки, сказаны мною выше. Там может находится логика обработки push уведомлений, логика входа с помощью отпечатка пальца и так далее. Каждый проект несет за собой свои особенности и фичи.

Но если вам скажут создать новый функционал, например реализовать передачу местоположения пользователя в фоном режиме, вы уже знаете что это будет описано по пути core/domain/repository и core/data/repository. В core потому это не является какой-то фичей приложения, у нее нет ui, она работает в любом месте, в любых условиях. В domain потому что у нас выполняется логика - достать место положение. Нам придется создать абстрактный класс:

abstract class Location {
	Location getLocation();
}

Далее в папке core/data/repository- мы уже выполняем его реализацию

class LocationImpl implements Location {
	@override
	Location getLocation(){
		//code
	};
}

Надеюсь, эта статья была для вас полезна, желаю вам удачи 😃