Козел отпущения или MVC в iOS

в 15:41, , рубрики: iOS, objective-c, swift, xcode, разработка под iOS

В последние несколько лет я встретил множество проектов, разработчиков и статей, которые обвиняют Model-ViewController архитектуру во многих, если не во всех бедах, которые происходят в iOS-сообществе.

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

Начнем с того, что такое MVC. Все мы видели эту красивую диаграмму из документации:

image

В теории все довольно красиво, так же как и на диаграмме, view передает действие пользователя controller’у, он в свою очередь если необходимо обновляет модель, модель после завершения обновления уведомляет controller, и он же обновляет view. Все просто, все работает. Используя данный подход были написаны десятки, если не сотни тысяч приложений, в том числе и крупные, и я имею ввиду не только iOS.
MacOS, Web приложения, Windows приложения и многое другое использует этот подход на протяжении уже почти 40 лет. Многие из них используют этот подход и сегодня, и при этом не испытывают никаких проблем.

Основные достоинства MVC — простота в использовании, особенно в iOS. UIViewController и его наследники в UIKit framework дают очень много функционала и возможностей “из коробки“. Для написания маленьких приложений практически не требуются навыки программирования, достаточно пары дней YouTube лекций и можно начинать. 
Проблемы же начинаются, когда вам нужно работать на больших проектах. Множество свидетельств того, что MVC как паттерн, очень плохо масштабируется, и это в свою очередь ведет к проблеме, которую называют Massive View Controller. 


Причины возникновения проблемы



Причинами же возникновения данной проблемы, являются на мой взгляд 2 вещи:

1. MVC действительно не дает вам четких инструкций о том, какие сущности и классы вам нужно создавать, а какие нет. Особенно в iOS. UIKit MVC изначально лишь дает вам UIViewController, и это в принципе и все. View существует, но не декларировано изначально, т.е. даже не создавая отдельный экземпляр view, имея только UIViewController приложение будет выполнять свои функции. То же самое и с моделью. Структура и архитектура модели, а так же ее взаимодействие с controller’ом остаться полностью на совести и воображении разработчика(ов). Это собственно и ведет ко второй причине.

2. Плохое понимание доменной области, неспособность разработчиков выделять необходимые сущности и как следствие накопление зависимостей и функционала внутри UIViewController. Т.е. когда разработчик добавляет новый функционал, вместо создания новых сущностей и рефакторинга существующей архитектуры — добавляется новый функционал во ViewController.

Т.е. я хочу сказать, что проблема возникает в равной степени из-за недостатков архитектуры и невнимательности/халатности/лени/незнания/неопытности (нужное подчеркнуть) разработчиков.

Решения первой проблемы

Довольно популярные в последние несколько лет подходы MVVM, MVP, VIP, VIPER, Flux(Facebook), Riblets (Viper от Uber), Clean-Swift и другие, частично или полностью решают первую часть проблемы MVC. Многие из них дают разработчику(ам) четкие инструкции о том какие классы и для чего необходимо создавать. Так же они значительно облегчают работу в команде, особенно если команда состоит из 10+ разработчиков, которые работают на одном проекте (например в Uber работает 150, JustEats 20+, Facebook даже считать боюсь, но точно более 10 ;) ). Облегчение происходит из-за того, что подразумевается создание большого количества изолированных объектов, только для покрытия Controller кода, это в свою очередь позволяет без особых потерь распределить работу внутри команды.

Вторая же проблема все еще остается. Я встретил множество проектов, которые использовали VIP и MVVM с классами view-models и presenters с более чем 1500 строк кода, содержащих множественные операции, начиная от парсинга и мапинга, заканчивая формированиями запросов в базы данных и http запросами. Проблема неопытности разработчика или непонимания архитектуры приложений в целом не решить простой сменой архитектурного паттерна. С моей точки зрения не существует “золотой пули”, которая позволит вам поддерживать/писать большие и сложные проекты просто “by default“. В любом случае это будет требовать усилий, юнит тестов, грамотного планирования и времени.


Та часть, где есть советы о том, как частично избегать второй проблемы

1. Облегчаем ViewController

Прежде всего, по хорошему, никакой объект в приложении не должен быть ответственным более чем за 1 функцию (Single Responsibility). Для многих разработчиков это очевидно, но тем не менее их код далеко от соблюдения такого простого постулата. Для многих не очевидно, что UIViewController — это такой же объект как и все остальные, и что у него должна быть только одна ответственность — связывать model и view. Т.е. поддерживать view в соответствующем модели состоянии и оповещать model о том, когда ей нужно менять это состояние. Все, что выходит за пределы этого определения, с моей точки зрения контроллеру не принадлежит. Весь мусор такой как анимации, layout, композиции view, изменение отрисовок, парсинг, мапинг, http запросы, запросы в базы данных, разного уровня сервисы операционной системы и многое другое — это все не принадлежит контроллеру. Ничего из этого не должно создаваться и вызываться внутри контроллера.
Одним из отдельных пунктов должна быть навигация. Если ваш ViewController имеет более чем одну точку выхода, то стоит задуматься о навигации и управлении навигацией. Очень часто приходилось встречать ViewController’ы которые имели 5-10 точек выхода разбросанных по всему коду controller’а. Существует множество способов как это можно организовать, от вынесения навигации в отдельные extension, управлении навигацией исключительно через segue, создании отдельной сущности Router, наследовании UINavigationController и тд. но это тоже нужно делать. И тестировать. 
Главное что нужно запомнить, что ViewController должен делать, то что он должен, а не все под ряд.
То же самое можно сказать и о сущностях, которые используются в MVVM, VIPER (список подлиннее чуть выше). Все они должны выполнять, только то что должны, если это View-Model — то только репрезентация данных для View, если это entity — то хранение и преобразование данных и тд(примеры ответственностей произвольные, автор не претендует на полную истинность этих утверждений). Всегда перед тем как добавить новый метод или свойство к уже существующему классу — задумайтесь о том, действительно ли это должно быть именно в этом классе.

2. Модели


Одна из ошибок, которую можно довольно часто наблюдать в iOS — это недостаточная изолированность уровня model. С моей точки зрения, и после успешного применения на практике, лучше всего модели выносить в отдельные framework’и. Выносить можно по доменной принадлежности. Для примера, если у вас приложение доставки еды — то в отдельные framework’и можно вынести например: рестораны и все что связано с ними(поиск по ресторанам, меню, расположение и тд), блюда, районы доставки и тд.
Если у вас приложение по покупке авиабилетов то можно выделить десяток farmework’ов (поиск билетов, резервирование, покупка, история и тд). Каждый framework можно покрыть отдельным сетом unit test. 
Можно этого и не делать, но тем не менее стараться писать модели полностью изолированные от любого взаимодействия с контроллерами.

3. Массивные ‘UI’ ViewController’ы


Я не один раз встречал огромные сториборды(к ним еще вернемся) и не менее огромные UIViewController’ы. Огромные не с точки зрения количества кода(хотя и это тоже), но с точки зрения количества UI элементов. 
Вопрос, который как я думаю должен задать себе каждый разработчик, когда он видит огромный, нагроможденный ViewController, звучит так — “Могу ли я как-то разбить этот VC на меньшие View или VC“.


image

Это скриншот пустого документа в Word, и я очень сомневаюсь, что это 1 ViewController, но при этом очень многие iOS разработчики следуют парадигме 1 экран — 1 ViewController.

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

Примером может быть вот этот экран.

image

Где каждая ячейка в таблице — отдельный VC. Заголовок с Аватаром автора, автором и временем — отдельный ViewController. Контент и Ссылка — тоже отдельный ViewController.

Like — Comment — Share — все эти компоненты — отдельный ViewController.


4. Много UI Кода

Объемный UI код так же может вести к ожирению разных компонентов. Решением может быть дефолтное наследование UI компонентов вместо кастомизации внутри ViewController’а. Или же вы можете добавлять extension’ы для описания UI.


Массивные Storyboard.

Множество раз я встречал массивные Storyboard файлы в проектах. По 20-50 ViewController с разными уровнями детализации внесены в Storyboard. Вначале все идет гладко и удобно, вся навигация тут как тут, настройки тоже, но как только к проекту меняются требования или же подключаются новые разработчики — это становится полным адом. 
Одно из решений — не использовать Xcode UI Builder как таковой. Создавать весь layout из кода.

Использовать различные библиотеки для упрощения написания constraints(Parus, Masonry т тд.) или же по хардкору использовать CGFrame и под все ориентации и разрешения писать все самому. 
Если же очень хочется использовать Storyboard то задумайтесь о том, чтобы разбить ваш проект на несколько. В идеале Storyboard должен иметь одну ответственность, или один usecase. Т.е. например если у вас приложение для Такси, то отдельными Storyboard’ами могут быть: Вызов такси, Отслеживание такси, Оплата, Общая информация, и тд. Т.е. более менее самостоятельные фичи. Со storyboard reference делать это очень удобно и требует минимум написания кода. В идеале Storyboard не должен содержать больше чем 3-4 отдельно взятых экрана. Чем больше становится storyboard тем больше сил требуется для его поддержки, в нем сложнее разобраться с первого раза, он банально дольше грузится и с ним становится тяжелее работать в группе разработчиков.


Выводы



Что хотелось сказать этим постом — это то, что я заметил тенденцию обвинения MVC во многих бедах iOS разработчиков, при этом очень часто(как видно на некоторых примерах выше), эти обвинения не особо относятся конкретно к iOS. Чаще же это вина самих разработчиков, более того, эти разработчики зачастую испытывают те же или схожие проблемы при переходе на другие архитектуры. Massive VC появляется в 90% случаев не потому, что MVC не умеет масштабироваться, а потому, что разработчики не хотят/не позволяют ограничения правильно масштабировать.

VIPER, MVVM (и другие) могут вам помочь с этим, особенно если вы работаете в команде разработчиков, но они не будут для вас панацеей от всех ваших проблем. В статье я лишь попытался привести некоторые из ситуаций в которых MVC — не виноват, и в своем роде выступить адвокатом дьявола, хоть сам и пользуюсь VIPER/MVVM на своих проектах. 
Пишите хороших код. Всем бобра.

Автор: PavelGatilov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js