Опровергаю пять архитектурных заблуждений

от автора

Привет! Я Алексей, iOS-разработчик в Тинькофф. Участвую в развитии архитектуры приложения, провожу собеседования и занимаюсь менторингом разработчиков.

За мой рабочий опыт у меня сложилось ощущение, что в среде мобильной разработки есть недопонимание ключевых принципов архитектуры. Хочется десакрализировать тему архитектуры, раскрыть некоторые принципы, по которым она строится, и разобрать популярные заблуждения, с которыми я столкнулся. Расскажу, почему бизнес-логика — это не все, кроме UI, с чего начинается архитектура и в чем разница между архитектурными шаблонами и архитектурой.

Правила и требования в архитектуре

Для простоты давайте считать архитектурой приложения набор правил, которыми мы руководствуемся при написании кода, не считая ТЗ. Сюда можно отнести не только System Design, паттерны и Code Style, но и внутренние договоренности команды не задействовать сторонние библиотеки, избегать хаков рантайма и использовать современное API нативных фреймворков

Главное, чтобы наши правила были четко зафиксированы в текстовом виде и не хранились только в голове у главного архитектора. Кроме текстовой формы важна и мотивация внедрения, чтобы не создавать карго-культ. Иначе, когда причина внедрения правил устареет или отпадет, они продолжат передаваться из уст в уста от код-ревью к код-ревью на автомате, как «сакральные знания предков». Через несколько лет сотрется из памяти, какую проблему решало каждое из правил.

Например, мы не используем популярный фреймворк X, потому что в нем находят редкие баги, которые долго правятся. А для нас критична скорость исправления багов. Значит, если владельцы фреймворка X изменят подход и будут гарантированно фиксить баги в течение пары дней, правило можно отменить, а фреймворк — использовать.

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

Любую архитектурную задачу можно сформулировать в виде вопросов. Как мы создаем модуль? Как передаем данные из одного модуля в другой? Где храним общие постоянные данные? Когда и как кэшируем сетевые запросы?

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

Требования бывают функциональными и нефункциональными. 

Функциональные требования говорят, что именно должен делать наш продукт:

Нефункциональные требования описывают, как именно продукт будет что-то делать:

— показать TODO-список для котиков;
— включать/выключать фонарик, когда рядом котик;
— отправлять/получать сообщения — конечно, с котиками.

— скорость компиляции;
— размер приложения;
— расширяемость.

Держать в голове все требования и приоритеты между ними — непосильная задача, особенно когда их много. Чаще всего требования сформулированы продуктово-бытовым языком, который разработчику нужно переводить в технические постановки. Поэтому нам, как разработчикам, гораздо удобнее скомпилировать все повторяющиеся задачи и хотелки сразу в архитектурные требования.

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

Мне ближе бизнесовый подход. С точки зрения коммерческой разработки наилучшая архитектура — та, что в докладе Роберта Мартина. К тому же архитектурные задачи, которые решал оригинальный MVC, устарели. 

Выбранный шаблон становится частью архитектуры всего приложения, но его одного недостаточно. Часто в статьях про мобильные архитектуры обсуждают только архитектуру UI-слоя: как передавать данные во View, получать фидбек от пользователя, как делать роутинг экранов и так далее. При этом остальная часть приложения словно не заслуживает внимания и остается за кадром. А это сбивает с толку, ведь MVC-подобные шаблоны говорят, только как отделить Ох уже эти всемогущие и безликие сервисы!

Ох уже эти всемогущие и безликие сервисы!

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

В идеале архитектура должна отвечать на любые вопросы, особенно если они часто возникают. Поэтому она и существенно выходит за рамки паттерна MVC. Так же как работа разработчика — нечто большее, чем написание строк кода.

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

Например, на нашем крупном проекте порядка 80 разработчиков. За месяц они добавляют или редактируют около 90 000 строк. На работу с кодом они в среднем тратят половину своего времени. Остальное — на созвоны, перемещение задачек в таск-менеджере, обсуждение ТЗ, налаживание процессов и прочие сторонние активности.

Тогда при общем количестве кодо-человеко-часов 80 × 20 × 4 = 6400 ч⋅ч в месяц выходит, что один разработчик задевает в среднем 14 строк в час. Причем среди этих строк есть и строки форматирования, и шаблонный код тестов, и другие несмысловые изменения вроде правки линтера, а также общий рефакторинг от платформенной команды. Осмысленных строк наберется от силы 5—7.

К аналогичным выводам приходят и другие разработчики на Stackoverflow.com, полагая, что в среднем 10—12 строк кода в день — нормальная практика. То есть и вовсе пара осмысленных строк в час. Такой показатель далек от скорости набора на клавиатуре любого разработчика. Даже если увеличить его раз в 5, получится только 20—30 смысловых строк, и это все равно довольно скромно. Выходит, что основное время разработчик тратит не на нажимание на клавиш. Но на что же тогда? По субъективному опыту, на первом месте будут: 

  • Чтение и понимание кода. Причем не понимание «что он делает?», а «почему делает именно так?». 

  • Поиск лучшего способа изменить этот код: добавить функциональность или исправить баг.

  • Подбор короткого, понятного имени для методов и свойств.

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

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

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

Именно с помощью доменных высокоуровневых моделей описывается бизнес-логика, в нашем примере это игровые правила. Неважно, в каком виде представлены доменные сущности: как картонка с текстом, json-объект или ООП-класс. Все они служат одной цели — описанию игрового процесса. И этот процесс должен правильно работать.

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

Другими словами, бизнес-логика — это такая логика, без которой ни игра, ни ваше приложение не имеют смысла. Бизнес-логика занимает центральную роль в вашем приложении.

Программы могут существовать без базы данных, сетевого слоя и, как ни странно, даже без UI. Но они не могут существовать без своей сути, своих сущностей. Список дел не может обойтись без объектов Task и List, а онлайн-магазин — без объектов Product, Cart и Order.

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

Самый верхний слой — это UI. При обсуждении архитектуры часто речь заходит о слоях и их зависимости друг от друга. Все согласны с принципом инверсии зависимости (буква D из SOLID), но не все его понимают в полной мере. Что считать модулем верхнего уровня? Что нижнего? И где вообще это направление?

Проверим нашу ориентацию в архитектуре (где верх, где низ) и попробуем сгруппировать в 4 слоя, от верхнего к нижнему, такие сущности: UI, Presenter, ViewModel, Interactor, Reducer, UseCase, Repository, NetworkingService, BluetoothService, DataBaseFrameworkService, Entity, DomainModel.

Получим примерно следующее:
  1. Entity и DomainModel.

  2. Interactor, Reducer, UseCase.

  3. Repository, Presenter, ViewModel.

  4. UI, NetworkingService, DataBaseService, BluetoothService.

Если у вас иначе, давайте разберемся.

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

A. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Если UI будет выше всех, он не должен зависеть от ViewModel и Presenter? Понятно, что они будут зависеть от общих абстракций (протоколов), но к какому слою должны относится эти абстракции — UI или Presenter? Чтобы соблюсти принцип, нам нужно разместить абстракции (интерфейсы) в верхнем слое, иначе нарушим правило A. 

Но если верхним будет UI, значит, все модули должны зависеть от него? Даже слой бизнес-логики, хотя и опосредованно? Надеюсь, понятно, что это ошибка и так лучше не делать. Поэтому при соблюдении DIP в шаблонах MVP и MVVM UI будет зависеть от слоя Presenter или ViewModel. 

Напоминалка по Чистой архитектуре

Чтобы легче ориентироваться в диаграмме чистой архитектуры, можно представить ее как вид сверху на детскую пирамидку. На самом верху — доменные сущности. От слоя сущностей зависит слой Use Case, где сосредоточена бизнес-логика. От слоя Use Case через прослойку интерфейсных адаптеров (Presenter, ViewModel) уже зависит слой фреймворков, где данные покидают наше приложение или, наоборот, приходят извне.
С бэкендом наше приложение общается через сетевой фреймворк (Foundation, Alamofire), с пользователем — через UI (UIKit, SwiftUI), с соседним телефоном — через Bluetooth (CoreBluetooth)

В UI логику не держим. Часто встречается утверждение, что UI должен быть максимально глупым и всю логику из него следует выносить. Это справедливо, только если имеется в виду бизнесовая логика. В любом другом случае слой может быть очень умным и сложным, если этого требует задача. Задача UI — получить данные от пользователя и рендерить картинки. И ничто не мешает ему инкапсулировать в себе всю сопутствующую логику.

Целые горы логики спрятаны от наших глаз и инкапсулированы в UIKit

Целые горы логики спрятаны от наших глаз и инкапсулированы в UIKit

С выходом iOS 15 Apple на примере AsyncImage показала, что можно доверить загрузку картинок и UI-слою. Многие разработчики обрадовались, некоторые засомневались. Были и те, кто задумался: корректное ли это решение? И если да, то только для SwiftUI или и для UIKit тоже?

Дело не только в желании упростить API для начинающих разработчиков. Такое решение имеет весомые причины и с точки зрения архитектуры. Рассмотрим эволюцию работы с картинками поэтапно:

  1. Первая версия приложения. Картинки для простоты хранятся в самом приложении.
    В какой-то момент приходят аналитики с недоумением: почему наше приложение с тремя экранами весит 200 МБ? Это сильно влияет на процент загрузок. Надо оптимизировать.

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

  3. Известная проблема — известное решение. Настраиваем кэширование картинок.

Каждый раз менялся только способ хранения ресурсов, бизнес-логика никак не затрагивалась. Более того, сетевая часть приложения по получению бизнесовых данных в виде JSON не изменилась. Да и вообще, в идеале наш бэкенд и CDN с хранилищем картинок — это буквально разные эндпоинты и даже разные ЦОДы. 

То есть перенос картинок в облако — это оптимизация UI-слоя. И раз оптимизируем только его, то все связанное с этой оптимизацией логично хранить в UI-слое. 

Как в таком случае работать с инвалидацией кэша, можете спросить вы. Отвечу так: это зависит от выбранного контракта. Все уже придумано за нас, и нам осталось выбрать. Есть и HTTP-коды (304 Not Modified), и специальные HTTP-заголовки (cache-control, tag). А если картинка не критичная, можно просто положиться на время жизни кэша 20 минут и не усложнять. Кэширование — решенная задача, изобретать свое решение не стоит.

В случае с https://habr.com/ru/articles/793398/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *