Это первая из статей, где мы рассказываем об архитектуре приложения Reddit под iOS. Здесь речь идёт о функциональности, которая работает ближе к UI. В частности, о переходе к архитектуре Model-View-Presenter (MVP). Преимущества такого рефакторинга:
- Улучшение гибкости кода, его ясности и поддерживаемости для поддержки будущего роста и ускорения итераций.
- Повышение производительности прокрутки в 1,58 раза.
- Стимуляция модульного тестирования. Количество тестов увеличилось с нескольких штук до более 200.
Ниже — наглядная схема нашей многоуровневой архитектуры. В первой статье сосредоточимся на уровнях View и Presenter.
Окончательный вид нашей многоуровневой архитектуры
Предпосылки для изменений
Более года назад мы опубликовали статью «Построение ленты в приложении Reddit для iOS». Там обсуждалось, как генерировать производительную, расширяемую ленту с замечательным показателем 99,95% сессий без сбоев. Мы объяснили, как используем архитектуру Model-View-Controller (MVC) и создаём абстракции для постраничного забора данных.
Сейчас Reddit вырос и продолжает расти как организация и как сервис. Следовательно, возросли требования к приложению Reddit iOS. Оно должно поддерживать больше запросов функций, более быстрые итерационные циклы и более высокие стандарты качества. Команда разработчиков выросла с трёх до более двадцати человек. Первоначальная архитектура MVC с трудом соответствует этим требованиям, так что пришлось вносить архитектурные изменения.
Суть проблемы
Со временем код потерял в гибкости и ясности. В сообществе iOS-разработчиков аббревиатуру MVC часто расшифровывают как Massive View Controller, потому что контроллеры представлений часто раздуваются до божественных объектов на тысячу с лишним строк. Несмотря на все наши усилия, проблема действительно возникла: иерархия наследования стала неудобно глубокой, а контроллеры стали превращаться в непонятные божественные объекты, которые сопротивляются изменениям.
Мы вбили последний гвоздь в гроб MVC, когда решили изменить уровень представления для ленты. Аудитория приложения Reddit всё растёт, поэтому производительность прокрутки стала слишком часто деградировать с 60 FPS до 45−55 FPS. Это значит, что нужно переписать слой представления ленты, при этом поддерживая исходную реализацию. Но в существующей архитектуре MVC мы не могли переписать слой представления ленты, не дублируя тысячи строк кода.
Кроме того, многие части кодовой базы с трудом поддаются тестированию. Код находится в трудно тестируемых класса слоя представления, а зависимости — часто в одиночках (синглтонах) или жёстко прописаны в самом классе. Мы хотели реализовать возможность нормального тестирования.
Другие задачи, поставленные перед рефакторингом: количество сбоев должно остаться низким, новая архитектура должна заложить основу для будущего роста и не помешать функциям, которые зависят от существующей инфраструктуры. То есть требуются эволюционные, а не революционные изменения.
Переход на MVP
Мы решили, что для решения вышеуказанных проблем необходима новая версия приложения. После рассмотрения нескольких вариантов мы решили использовать архитектуру Model-View-Presenter (MVP). MVP соответствует всем названным критериям, к тому же это хорошо известная и документированная архитектура, так что легче обучать инженеров. В ней также сохраняется концепция «моделей просмотра» (view models). При необходимости в Presenter можно создавать объекты модели представления по принципу единственной ответственности — и использовать их для расширения наших представлений.
Диаграмма Model-View-Presenter
Избавляемся от Massive View Controller
Для iOS-приложений принято, что объекты представлений — это подклассы UIView, объекты контроллеров — подклассы UIViewController, а объекты моделей — простые объекты ol. Как понятно из названия UIViewController, здесь представление и контроллер объединяются в одном объекте. То есть модель MVC на iOS часто теряет свои преимущества из-за жёсткой связи уровня представления и контроллера. Интересно, что сама Apple признаёт эту связь.
Часто архитектура Model-View-Controller под iOS превращается в такое
В архитектуре MVP мы учитываем это понятие и формализуем его, рассматривая UIViewController действительно как явный объект слоя представления. Концепция обработки UIViewController как объекта представления с неудачным названием в последние годы стала популярной.
Итак, удаляем всю постороннюю логику в наших UIViewController’ах. Затем назначаем Presenter на роль посредника между представлением и моделью. В этой новой роли он не знает об объектах представления, таких как UIViewController. Обратите внимание, что Presenter взаимодействует с представлением через интерфейс. Теоретически, можно поменять реализацию представления на NSViewController (для MacOS) и др.
Облегчаем ViewController, введя Presenter и разделив обязанности
Обдумываем варианты MVP
Как видно на диаграмме MVP, архитектура вышла очень похожей на MVC. Действительно, сходств больше, чем различий. Такая архитектура просто помогает установить правильное разделение кода презентации и бизнес-логики, как которому стремится MVC. На самом деле, все производные архитектуры МВ(х), таких как MVP, MVVM, MVAdapter и другие — просто разные варианты одной концепции.
Можно задать вопрос, почему мы вообще отказались от MVC. На самом деле, Apple описывает разные виды контроллеров: для моделей, посредников и координации. Честно говоря, может мы и могли заменить наш Presenter другим контроллером. Но решили этого не делать, потому что у большинства разработчиков iOS по ряду причин сформировалось убеждение, что UIViewController — синоним контроллера. Используя слово Presenter, мы как бы даём сигнал, что этот объект существенно отличается от обычного контроллера с определенным набором функций и свойств.
Улучшение гибкости, поддерживаемости и ясности
«Предпочитайте композицию наследованию» — известная мантра в объектном программировании. С наследованием вам нужно предсказывать будущее и строить огромную таксономию объектов. Но если ваша «идеально» построенная иерархия наследования начинает разваливаться из-за непредвиденных изменений, трудно модифицировать эту жёсткую структуру. В композиции объекты создаются из других объектов и делегируют им работу. Это полезно, потому что так поведение объекта легко изменить во время выполнения, просто поменяв объекты, из которых он состоит. Эти составные объекты ещё и понятнее, поскольку код вытесняется из иерархии наследования в абстракцию, ориентированную на одну конкретную задачу.
Такая композиционность — одно из главных преимуществ, которое дала нам архитектура MVP. Теперь можно поменять поведение контроллера просто изменив состав конкретного Presenter. Нас теперь меньше беспокоит расшифровка сложной и жёсткой структуры наследования. Наконец, контроллеры представлений и объекты Presenter легче понять, поскольку у них более чёткий набор задач.
Введя Presenter и перенеся туда часть логику контроллера представлений, мы упростили иерархию наследования контроллера. На рисунке ниже видно, что удалось удалить класс GalleryFeedViewController, поскольку мы поместили всю эту логику в Presenter. Как уже обсуждалось, такая иерархия наследования проще для понимания и менее жёсткая.
Упрощение иерархии наследования через композицию
Свободное изменение реализации слоя представления
Как обсуждалось ранее, производительность скроллинга ленты начала снижаться с 60 FPS до 45−55 FPS. Поэтому для слоя представления ленты мы решили использовать Texture. Это платформа с открытым исходным кодом на базе Apple UIKit, повышающая производительность интерфейса за счёт предварительной обработки в фоновом потоке. В прошлой архитектуре MVC мы не могли изменить реализацию уровня представления без массы дублирования кода.
Перед внедрением MVP нужно было дублировать в ViewController посторонний код, который не относится к View (оранжевый)
Новая архитектура MVP позволила внедрить поддержку Texture, а не переписывать вещи с нуля. Мы просто поместили всю логику, не связанную с View, в общий класс Presenter. Затем написали новую реализацию слоя представления c Texture и повторно использовали код Presenter. Это дало поддержку обеих реализаций View до тех пор, пока не пришло время комфортно выкатить ленту с Texture для всех пользователей.
После реализации MVP: код, который не относится к View, перемещён в совместно используемый Presenter
Каков результат? На диаграмме внизу показано увеличение производительности прокрутки ленты. Мы хотели остаться в районе 60 FPS, чтобы добиться абсолютно плавной прокрутки.
Модульное тестирование
Конечно, мы внедрили модульные тесты не только из-за MVP, но это был важный фактор. В частности, архитектура MVP увеличила область тестирования за счёт перемещения кода на уровень, где его легче проверять. Побочный эффект в том, что уровни View стали проще — и, следовательно, их реже требуется тестировать.
Увеличение области тестирования после переноса кода, который не относится к View, за пределы этого слоя
Модульные тесты улучшили поддерживаемость кодовой базы: они позволяют более уверенно вносить изменения и помогают понять, каким должно быть правильное поведение. Они также делают код более гибким и понятным, потому что поощряют такие методы, как внедрение зависимостей, композиция и программирование абстракций. Количество юнит-тестов выросло с нескольких штук до более 200.
Критический анализ MVP в Reddit
Хотя переход на MVP сильно помог, но остались некоторые вещи, которые следует изучить.
Переход ленты на Texture вызвал новые проблемы с потоками. Приложение изначально не поддерживало асинхронную реализацию View. То есть неизбежно появляются ошибки в случае несоответствия между состоянием View и состоянием приложения. Например, в представлении ленты может быть N записей. А в фоновом потоке состояние приложения незаметно изменилось — и теперь содержит менее N сообщений. Если не устранить несоответствие, то приложение просто вылетит с ошибкой, когда View попытается отобразить N-й пост в ленте.
Исправить ошибки с потоками труднее всего. Их сложно воспроизвести, поэтому они плохо поддаются отладке. Пришлось изменить логику запроса и получения данных для просмотра ленты. В частности, мы реализовали «защиту» с запретом любых изменений источника данных, когда View ленты претерпевает какие-то изменения. Это и другие мелкие исправления уменьшили число ошибок, связанные с потоковой обработкой. Тем не менее, асинхронную многопоточность можно ещё улучшить.
Во-вторых, слой Presenter представляет собой лишний «шаг» в конвейере. Этот шаг имеет определённую цену с точки зрения увеличения сложности кода и снижения производительности. Иногда вам хочется просто выполнить эту логику в UIViewController из прихоти или потому что привыкли так делать. В самом неудачном случае вы обнаружите, что Presenter присутствует просто как сущность, не имея никакой значимой логики. В такой ситуации Presenter, похоже, не оправдывает своё существование.
Иногда можно перейти от слоя View к слою RedditCore без участия Presenter
На самом деле наше приложение не полностью преобразовано в архитектуру MVP. Во-первых, преобразование каждого отдельного UIViewController в Presenter будет слишком трудоёмким — и не эволюционным изменением. Во-вторых, как было сказано в предыдущем абзаце, иногда Presenter просто не нужен. Как мы обнаружили в работе по внедрению Texture для ленты, Presenter отлично подходит для облегчения массивного MVC или для реализации View с переменным поведением, или если у вас сложная логика, которую нужно проверить. Но иногда UIViewController настолько прост, что в Presenter нет смысла. Так что он необязателен. Presenter следует реализовать только при необходимости.
Резюме и планы на будущее
Рефакторинг архитектуры MVP в приложении Reddit для iOS помог решить многие поставленные задачи. Введя уровень Presenter, мы постепенно развили архитектуру приложения для поддержки новой реализации уровня представления, не нарушив работу других функций. Код стал понятнее за счёт облегчения «массивного MVC» — переноса посторонней логики в слой Presenter. Мы также дали разработчикам возможность более быстрых итераций и развёртывания новых функций. И значительно улучшили тесты.
Учитывая всё это, предстоит ещё долгий путь. Мы продолжаем создавать объекты Presenter и совершенствуем их. Нужно продолжить перемещать постороннюю логику из UIViewController’ов на уровень Presenter. Также необходимо, чтобы все Presenter’ы лучше соответствовали принципу единственной ответственности. В конце концов, и приложение, и архитектура эволюционируют постоянно.
ссылка на оригинал статьи https://habr.com/post/423531/
Добавить комментарий