Ios mvp mvc mvvm кратко

Обновлено: 02.07.2024

Шаблоны архитектуры iOS - краткое введение в MVC, MVP, MVVM и VIPER (перевод)

Чувствуете себя странно при использовании iOS MVC? Хотите попробовать MVVM? Я слышал о VIPER раньше, но мне интересно, стоит ли учиться?

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

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

Изучение шаблонов проектирования вызывает привыкание, поэтому сначала напомню: после прочтения этой статьи у вас может возникнуть больше вопросов, чем раньше, например:

(MVC) Кто отвечает за сетевые запросы: модель или контроллер?

(MVVM) Как передать модель в модель представления вновь созданного представления?

(VIPER) Кто отвечает за создание модуля VIPER: маршрутизатор или презентатор?

Зачем заботиться о выборе архитектуры?
Потому что, если вам все равно, трудно гарантировать день, вам нужно отладить огромный класс с различными проблемами, и тогда вы обнаружите, что в этом классе вы его вообще не найдете Это не исправит никаких ошибок. Вообще говоря, очень трудно запомнить такой большой класс в целом, и вы всегда забудете некоторые из более важных деталей. Если вы обнаружите, что эта ситуация начала появляться в вашем приложении, то вы, вероятно, столкнулись со следующими проблемами:

Этот класс является подклассом UIViewController.
Ваши данные напрямую хранятся в UIViewController.
Ваши UIViews, кажется, ничего не делают.
Ваша модель - это просто структура данных
Ваш модульный тест ничего не охватывал
Фактически, даже если вы будете следовать спецификациям Apple по разработке и внедрять инфраструктуру Apple MVC, вы все равно столкнетесь с этими проблемами, так что терять нечего. У инфраструктуры Apple MVC есть свои недостатки, но об этом мы поговорим позже.

Давайте сначала определим характеристики хорошей структуры:

При строго определенных ролях обязанности распределяются между различными субъектами сбалансированным образом.
Тестируемость обычно зависит от первого пункта, упомянутого выше (не беспокойтесь слишком сильно, это не сложно сделать, если архитектура когда).
прост в использовании и не требует больших затрат на обслуживание.
Почему это разделено?
Когда мы пытаемся понять, как все работает, разделение может ослабить наше мозговое давление. Если вы чувствуете, что чем больше вы развиваетесь, тем больше ваш мозг может адаптироваться к сложным задачам, и это правда. Но эта способность мозга не увеличивается линейно, и вскоре она достигнет узкого места. Поэтому для решения сложных вопросов наилучшим способом является разделение своих обязанностей на несколько организаций при условии соблюдения принципа единой ответственности.

Почему тестируемость?
Для тех, кто благодарен за модульное тестирование, не должно быть никаких сомнений по этому поводу: модульное тестирование помогает им проверять ошибки в новых функциях или помогает им находить Исправлена ​​ошибка в сложном классе. Это означает, что эти модульные тесты помогают этим разработчикам находить проблемы до запуска программы. Если эти проблемы игнорируются, они, вероятно, будут отправлены на устройство пользователя, и для устранения этих проблем потребуется не менее недели (AppStore Аудит).

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

Основные элементы MV (X)
Теперь у нас есть много вариантов, когда мы сталкиваемся с шаблонами архитектурного проектирования:

MVC
MVP
MVVM
VIPER
Прежде всего, первые три режима классифицируют все объекты в одну из следующих трех категорий:

Модели - уровень данных или уровень интерфейса данных, отвечающий за обработку данных. Такие как классы Person и PersonDataProvider
Представления-Отображение слоя (GUI). Для iOS все классы, начинающиеся с пользовательского интерфейса, в основном принадлежат этому слою.
Controller / Presenter / ViewModel (модель контроллера / презентатора / представления) - это связующее звено или посредник между Model и View. Вообще говоря, когда пользователь выполняет операции с представлением, он отвечает за изменение соответствующей модели, а когда изменяется значение модели, он отвечает за обновление соответствующего представления.
После классификации объектов мы можем:

Лучшее понимание
повторное использование (в основном представление и модель)
, чтобы проверить их независимо
Позвольте мне начать с серии MV (X) и поговорить о VIPER в конце.

MVC - так оно и есть

2460743-9d2e0857e78bd026.jpg

Прежде чем начать обсуждать MVC от Apple, давайте взглянем на традиционный MVC.

При этой архитектуре представление не имеет состояния. Когда модель изменяется, контроллер просто перерисовывает ее, как и веб-страница, при нажатии на новую ссылку вся веб-страница перезагружается. Хотя эта архитектура может быть реализована в приложениях iOS, поскольку три объекта MVC тесно связаны, и каждый объект связан с двумя другими, это бессмысленно, даже если он реализован. Такая тесная связь также значительно снижает вероятность их повторного использования, что, вероятно, не то, что вы хотите видеть в своем приложении. Таким образом, я думаю, что нет необходимости писать традиционные примеры MVC.

Традиционный MVC больше не подходит для текущей разработки iOS.

Apple, MVC
идеально

2460743-e26f2df705596d8a.jpg

Представление и Модель независимы друг от друга, они взаимодействуют друг с другом только через контроллер. Немного раздражает, что Controller наименее пригоден для повторного использования, потому что мы обычно не помещаем сложную бизнес-логику в Model, поэтому мы можем поместить ее только в Controller.

В теории это кажется довольно простым, но вы думаете, что-то не так? Вы даже слышали, как люди называют MVC тяжелым шаблоном контроллера. Кроме того, похудение ViewController стало горячей темой среди разработчиков iOS. Почему Apple продолжает использовать традиционную архитектуру MVC с небольшим улучшением?

2460743-c985ea05b91971e5.jpg

Cocoa MVC рекомендует вам переписать контроллер, поскольку он необходим для управления всем жизненным циклом представления, и для контроллера и представления трудно быть независимыми друг от друга. Хотя вы можете делегировать часть бизнес-логики и преобразование данных в контроллере для Модели, когда вы хотите распределить нагрузку на View, нет никакого способа, потому что основная ответственность View заключается в том, чтобы говорить о рабочем поведении пользователя. Пусть Контроллер справится с этим. Таким образом, ViewController в конечном итоге становится прокси-сервером и источником данных для всего, и даже отвечает за инициирование и отмену сетевых запросов, и . все остальное для вас.

Вы должны быть знакомы со следующим кодом:

Ячейка как представление напрямую использует модель для завершения своей конфигурации. Принцип MVC был нарушен. Такая ситуация существовала всегда, и никто не думает, что есть какие-либо проблемы. Если вы строго следуете MVC, вы должны сконфигурировать ячейку в ViewController, а не бросать модель непосредственно в ячейку. Конечно, это сделает ваш ViewController более тяжелым.

Проблема не начинала проявляться до окончания модульного тестирования (надеюсь, уже в вашем проекте). Контроллер очень сложно протестировать, потому что он слишком связан с View. Чтобы протестировать его, вам нужно часто имитировать жизненные циклы View и View, и если вы пишете код контроллера в соответствии с этой архитектурой, код бизнес-логики также будет Это становится очень грязным из-за кода макета представления.

Давайте посмотрим на пример на этой площадке:

Сборка MVC может быть помещена в текущий отображаемый ViewController

Этот код не выглядит хорошо для тестирования, верно? Мы можем поместить метод генерации приветствия в новый класс GreetingModel для тестирования отдельно. Но если мы не будем вызывать связанные с View методы (viewDidLoad, didTapButton), мы не сможем проверить какую-либо логику отображения в GreetingViewController (хотя в приведенном выше примере очень мало логики), и если мы вызываем ее, нам может потребоваться изменить все Все представления загружены, что слишком плохо для модульного тестирования.

Взаимодействие между View и Controller не может быть действительно охвачено модульными тестами.

Таким образом, какао MVC не кажется хорошим выбором. Но давайте оценим его производительность в разных аспектах (я говорил об этом в начале статьи):

Partition-View и Model действительно разделены, но View и Controller слишком связаны
Тестируемость - поскольку разделение недостаточно ясно, можно протестировать только модель.
Простой в использовании - по сравнению с другими режимами, он имеет наименьшее количество кода. И в основном все знакомы с этим, даже разработчики с небольшим опытом могут поддерживать его.
В этом случае вы можете выбрать Cocoa MVC: вы не хотите тратить слишком много времени на архитектуру, и вы считаете, что это более высокая стоимость обслуживания для вашего небольшого проекта Это просто трата.

Если вы больше всего цените скорость разработки, то Cocoa MVC - ваш лучший выбор.

MVP-Обещания доставлены Какао MVC

2460743-6109619d7a996b10.jpg

Это очень похоже на MVC от Apple, верно? Это действительно очень похоже, его зовут MVP (Passive View). Подождите минутку . Значит ли это, что MVC от Apple на самом деле MVP? Нет. Напомним, что в MVC View и Controller тесно связаны, но для Presenter в MVP он вообще не обращает внимания на жизненный цикл ViewController, и View также можно просто отключить, поэтому в Presenter практически ничего нет. Связанный с версткой код, его обязанность заключается только в обновлении View через данные и статус.

Если я скажу вам, что роль UIViewController здесь на самом деле View, как вы себя чувствуете.

В архитектуре MVP эти подклассы UIViewController фактически принадлежат View, а не Presenter. Это различие обеспечивает отличную тестируемость, но оно достигается ценой скорости разработки, потому что вы должны вручную создавать данные и связывать события, как в следующем коде:

Важные замечания по сборке
Архитектура MVP имеет три действительно независимых уровня, поэтому при сборке будут возникать некоторые проблемы, и MVP стала первой архитектурой, которая обнаружит такие проблемы. Поскольку мы не хотим, чтобы представление знало информацию о модели, сборка в текущем ViewController определенно не правильна (на самом деле это роль представления). Мы должны завершить сборку в другом месте. Например, мы можем создать службу маршрутизатора для всего приложения и позволить ей отвечать за переходы сборки и просмотра между видами. Эта проблема существует не только в MVP, но и в шаблоне, который будет представлен далее.

Давайте посмотрим на производительность MVP в различных аспектах:

Division-Мы назначаем большую часть обязанностей Ведущему и Модели, и Представление в основном не должно ничего делать (в приведенном выше примере Модель ничего не делает).
Тестируемость - отлично, мы можем проверить большую часть бизнес-логики с помощью View.
Простота в использовании. Что касается нашего простого примера, приведенного выше, объем кода почти вдвое больше, чем в архитектуре MVC, но идея MVP вполне ясна.
Архитектура MVP в iOS означает отличную тестируемость и огромное количество кода.

MVP - добавлена ​​еще одна версия привязки данных

2460743-f14527aa59356803.jpg

Есть еще MVP-Контролирующий контроллер MVP. Эта версия MVP включает прямую привязку View и Model.В то же время Presenter (Supervising Controller) продолжает обрабатывать пользовательские операции в View и контролирует изменения отображения View.

Но, как мы уже говорили, нечеткое разделение обязанностей - это плохо, например, тесная связь View и Model. Этот принцип тот же в разработке приложений для настольных компьютеров Какао.

Как и в случае традиционной архитектуры MVC, я не могу найти причины для написания примера этой ошибочной архитектуры.

MVVM - самая новая и выдающаяся архитектура серии MV (X)

2460743-ed6c5536a908fed3.jpg

Архитектура MVVM является самой новой в MV (X). Будем надеяться, что когда она появится, будут рассмотрены проблемы, возникшие до появления режима MV (X).

Теоретически Model-View-ViewModel выглядит великолепно. Мы уже знакомы с View и Model, и мы знакомы с ролью посредника, но здесь роль посредника становится ViewModel.

Это очень похоже на MVP:

Архитектура MVVM рассматривает ViewController как представление.
Нет тесной связи между представлением и моделью
Кроме того, он также выполняет привязку данных, как версия MVP Supervising, но на этот раз он не привязывает View и Model, а привязывает View и ViewModel.

Итак, что такое ViewModel в iOS? По сути, он не зависит от UIKit, представления состояния View и View. ViewModel может активно вызывать для внесения изменений в модель, а также может настраиваться при обновлении модели, а затем через привязку между View и ViewModel представление также обновляется соответствующим образом.

привязывать
Я кратко упомянул это содержание в разделе MVP, давайте обсудим его дальше. Концепция связывания берет свое начало с разработки платформы OS X, но на платформе iOS у нас нет соответствующих инструментов разработки. Конечно, у нас также есть KVO и уведомления, но не удобно использовать эти методы для привязки.

Итак, если мы не хотим писать их сами, вот два варианта:

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

2460743-ffc90d911c88f3cd.jpg

В следующем небольшом примере использование Responsive Framework (FRF) или KVO является тривиальным использованием, поэтому мы используем другой метод: напрямую вызывать метод showGreeting в ViewModel, чтобы обновить себя (атрибут приветствия), ( В обратном вызове didSet свойства приветствия) воспользуйтесь функцией закрытияreetingDidChange, чтобы обновить отображение представления.

Затем мы вернемся и сделаем оценку его эффективности во всех аспектах:

Разделение - это не очень ясно в нашем маленьком каштане, но View в структуре MVVM отвечает за большее, чем MVP. Поскольку первый обновляет свое собственное состояние посредством привязки данных ViewModel, а второй просто передает все события Presenter для обработки и не несет ответственности за само обновление.
Тестируемость - поскольку ViewModel ничего не знает о View, наше тестирование на нем становится очень простым. View также может быть протестирован, но, возможно, из-за его зависимости от UIKit, вы пропустите его напрямую.
Простота использования. В нашем примере объем кода в основном такой же, как у MVP, но в реальных приложениях MVVM будет более лаконичным. Поскольку в MVP вам необходимо передать все события View в Presenter для обработки и вам необходимо вручную обновить состояние View, тогда как в MVVM вам нужно только использовать привязку для его решения.
MVVM действительно привлекателен, потому что он не только сочетает в себе преимущества нескольких вышеупомянутых фреймворков, но и не требует написания дополнительного кода для обновления представления (поскольку вы уже сделали Связывание данных), и его производительность в тестируемости все еще велика.

VIPER-Примените опыт построения блоков Lego к дизайну приложений для iOS

2460743-f46ccb62155b1967.jpg

VIPER - последний фреймворк, который мы представим. Интересно, что этот фреймворк не принадлежит ни к какому виду фреймворка MV (X).

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

Если мы сравним ряды VIPER и MV (X), мы обнаружим, что они имеют следующие различия в распределении обязанностей:

Логика Model (взаимодействие с данными) передается Interactor, а Entities - это просто структура данных, которая ничего не делает.
В обязанности Controller / Presenter / ViewModel, только функция отображения пользовательского интерфейса была передана Presenter. Presenter не имеет возможности напрямую изменять данные.
VIPER - это первый архитектурный шаблон, который разделяет обязанности навигации, а уровень маршрутизатора отвечает за навигацию.
Как правильно использовать навигацию (выполнять маршрутизацию) - это сложная задача для разработки приложений для iOS. Архитектура серии MV (X) полностью не знает (поэтому нет необходимости иметь дело) с этой проблемой.

Следующий пример не включает переход между навигационными модулями и модулями VIPER, а также не включает вышеупомянутую архитектуру серии MV (X).

Давайте оценим его производительность в различных аспектах:

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

Поэтому в одном приложении, даже если есть несколько смешанных архитектурных шаблонов, это нормально. Например: вначале вы использовали архитектуру MVC, а позже поняли, что с MVC создана специальная страница, поддерживать ее будет довольно сложно, в настоящее время вы можете использовать режим MVVM для разработки только для этой страницы. Для тех страниц, которые могут нормально работать с MVC, вам вообще не нужно их реорганизовывать, потому что две архитектуры могут гармонично сосуществовать.


Делаете все по MVC, а получается некрасиво? Сомневаетесь, переходить ли на MVVM? Слышали о VIPER, но не уверены, стоит ли оно того?

В этой статье я кратко рассмотрю некоторые популярные архитектурные паттерны в среде iOS и сравню их в теории и на практике. Больше информации вы найдете при переходе по ссылкам, указанным в тексте.

Освоение паттернов может вызвать зависимость, так что будьте осторожны: в
конечном итоге вы, возможно, станете задавать себе больше вопросов, чем до прочтения этой статьи, например:
— Кто должен владеть сетевыми запросами: Model или Controller?
— Как я могу передать Model во ViewModel нового View?
— Кто создает новый модуль VIPER: Router или Presenter?


Почему стоит позаботиться о выборе архитектуры?

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

Почему распределение?

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

Почему тестируемость?

Почему простота использования?

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

Основы MV(X)

  • Models — ответственные за данные домена или слой доступа к данным, который манипулирует данными, например, класс Person или PersonDataProvider;
  • Views — ответственные за уровень представления (GUI); для окружающей среды iOS это все, что начинается с префикса UI;
  • Controller / Presenter / ViewModel — посредник между Model и View; в целом отвечает за изменения Model, реагируя на действия пользователя, выполненные на View, и обновляет View, используя изменения из Model.
  • лучше понимать их;
  • повторно их использовать (в основном применимо к View и Model);
  • тестировать их отдельно друг от друга.

Как было раньше


Традиционный MVC кажется неприменимым к современной iOS разработке.

MVC от Apple

Ожидания


Controller является посредником между View и Model, следовательно, две последних не знают о существовании друг друга. Поэтому Controller трудно повторно использовать, но это, в принципе, нас устраивает, так как мы должны иметь место для той хитрой бизнес-логики, которая не вписывается в Model.

В теории все выглядит очень просто, но вы чувствуете, что что-то не так, верно? Вы наверняка слышали, что люди расшифровывают MVC как Massive View Controller. Кроме того, разгрузка ViewController стала важной темой для iOS-разработчиков. Почему это происходит, если в Apple просто взяли традиционный MVC и немного его улучшили?

Реальность


Сколько раз вы видели такой код:

View-ячейка конфигурируется непосредственно с Model. Таким образом нарушаются принципы MVC, но такой код можно увидеть очень часто, и, как правило, люди не понимают, что это неправильно. Если вы строго следуете MVC, то должны настраивать ячейку внутри контроллера и не передавать Model во View, что увеличит Controller еще больше.

Cocoa MVC обосновано расшифровывают как Massive View Controller.

Проблема не очевидна, пока дело не доходит до юнит-тестов (надеюсь, что в вашем проекте оно все же доходит). Так как View Controller тесно связана с View, ее становится трудно тестировать, и приходится идти изощренным путем, заменяя View Mock-объектами и имитируя их жизненный цикл, а также писать код View Controller таким образом, чтобы бизнес-логика была по максимуму отделена от кода view layout.

Кажется, это сложно протестировать, не так ли? Мы можем выделить генерацию приветствия в новый класс GreetingModel и тестировать ее отдельно, но мы не можем протестировать логику представления (хоть в примере ее не так много) внутри GreetingViewController без вызова методов жизненного цикла View напрямую (viewDidLoad, didTapButton), что может привести к загрузке всех UIView, и это плохо для юнит-тестов.

На самом деле, тестирование UIViews на одном симуляторе (например, iPhone 4S) не гарантирует, что он будет работать нормально на других устройствах (например, iPad), так что я рекомендую убрать галочку Host Application из конфигурации таргета юнит-тестов и запускать их на симуляторе, не включая само приложение.

Взаимодействие между View и Controller на самом деле не особо поддается тестированию с помощью юнит-тестов.

  • распределение: View и Model на самом деле разделены, но View и Controller тесно связаны;
  • тестируемость: из-за плохого распределения вы, вероятно, будете тестировать только Model;
  • простота использования: наименьшее количество кода среди других паттернов. К тому же он выглядит понятным, поэтому его легко может поддерживать даже неопытный разработчик.

Cocoa MVC является лучшим архитектурным паттерном с точки зрения скорости разработки.

Реализация обещаний Cocoa MVC



С точки зрения MVP, подклассы UIViewController на самом деле есть View, а не Presenter. Это различие обеспечивает превосходную тестируемость, которая идет за счет скорости разработки, потому что вы должны связывать вручную данные и события именно между View и Presenter, как можно увидеть на примере ниже.

Важное примечание относительно сборки

MVP является первым паттерном, выявляющим проблему сборки, которая происходит из-за наличия трех действительно отдельных слоев. Так как нам не нужно, чтобы View знала о Model, выполнять сборку в презентующей View Controller (который на самом деле View) неправильно, следовательно, это нужно сделать в другом месте. Например, можно создать сервис Router, который будет отвечать за выполнение сборки и презентацию View-to-View. Эта проблема возникает не только в MVP, ее также нужно решать во всех последующих паттернах.

  • распределение: большая часть ответственности разделена между Presenter и Model, а View ничего не делает;
  • тестируемость: отличная, мы можем проверить большую часть бизнес-логики благодаря бездействию View;
  • простота использования: в нашем нереально простом примере количество кода в два раза больше по сравнению с MVC, но в то же время идея MVP очень проста.


Существует другой вариант MVP — MVP с надзирающим контроллером. Он включает в себя прямое связывание View и Model, в то время как Presenter (надзирающий контроллер) по-прежнему обрабатывает действия с View и способен изменять ее.


Но, как мы уже узнали ранее, расплывчатое разделение ответственности плохо само по себе, равно как и тесная связанность между View и Model. А я не вижу смысла в написании примера для плохой архитектуры.

Самая новая из MV(X) вида.

MVVM является новейшим из MV(X) паттернов, так что будем надеяться, что он появился с учетом всех проблем, присущих MV(X).

В теории Model-View-ViewModel выглядит очень хорошо. View и Model уже нам знакомы, как и View Model в качестве посредника.


  • MVVM рассматривает View Controller как View;
  • в нем нет тесной связи между View и Model.

Так что такое View Model в среде iOS? Єто независимое от UIKit представление View и ее состояния. View Model вызывает изменения в Model и самостоятельно обновляется с уже обновленной Model. И так как биндинг происходит между View и View Model, то первая, соответственно, тоже обновляется.

Биндинги

  • одну из биндинг-библиотек, основанных на KVO (например,RZDataBinding или SwiftBond);
  • полноразмерный фреймворк для функционального реактивного программирования, такой как ReactiveCocoa, RxSwift или PromiseKit.

Есть одна горькая правда об FRP-фреймворках: великая сила приходит с большой ответственностью. Очень легко все поломать, когда вы пишете реактивно. Другими словами, если что-то пошло не так, вы можете потратить много времени на отладку приложения. Стоит просто взглянуть на этот стек вызовов.


VIPER

Опыт строительства из кубиков Lego, перенесённый на проектирование iOS-приложений

VIPER — наш последний кандидат, который особенно интересен, потому что он не из категории MV(X).

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


  • Interactor содержит бизнес-логику, связанную с данными (Entities): например, создание новых экземпляров сущностей или получение их с сервера. Для этих целей вы будете использовать некоторые Сервисы и Менеджеры, которые рассматриваются скорее как внешние зависимости, а не как часть модуля VIPER.
  • Presenter содержит бизнес-логику, связанную c UI (но UIKit-независимую), вызывает методы в Interactor.
  • Entities — простые объекты данных, не являются слоем доступа к данным, потому что это ответственность слоя Interactor.
  • Router несет ответственность за переходы между VIPER-модулями.
  • логика из Model (взаимодействие данных) смещается в Interactor, а также есть Entities — структуры данных, которые ничего не делают;
  • из Controller, Presenter, ViewModel обязанности представления UI переехали в Presenter, но без возможности изменения данных;
  • VIPER является первым шаблоном, который пробует решить проблему навигации, для этого есть Router.

В примере нет маршрутизации или взаимодействия между модулями, так как эти темы совсем не охвачены MV(X)-паттернами.

  • Распределение. Несомненно, VIPER является чемпионом в распределении обязанностей.
  • Тестируемость. Здесь нет ничего удивительного: лучше распределение — лучше тестируемось.
  • Простота использования. Как вы уже догадались, первые два преимущества идут за счет стоимости сопровождения. Вам придется писать огромное количество интерфейсов для классов с незначительными обязанностями.

Так что там c Lego?

При использовании VIPER вам может показаться, что вы строите Эмпайр Стейт Билдинг из кубиков Lego, и это говорит о том, что у вас есть проблемы. Может быть, вы слишком рано взялись за VIPER и стоит рассмотреть что-то попроще. Некоторые люди игнорируют это и продолжают стрелять из пушки по воробьям. Я предполагаю, что они верят, что их приложения получат выгоду из VIPER когда-нибудь в будущем, даже если сейчас стоимость обслуживания неоправданно высока. Если вы считаете, что оно того стоит, то я рекомендую вам попробовать Generamba — инструмент для генерации скелетонов VIPER. Хотя лично мне кажется, что это сродни использованию автоматического прицела для стрельбы из той же пушки вместо рогатки.

Вывод

Английская версия доступна здесь. Слайды которые я презентовал на NSLondon доступны здесь.

В этом цикле из трёх статей я кратко разберу все популярные архитектурные паттерны, использующиеся в iOS разработке: устройство, плюсы и минусы, а также когда и где их лучше применять. Собеседующим — хитрые вопросы, собеседуемым — клёвые ответы!

Первая часть посвящена MV(X) паттернам: самым известным и распространенным практикам в индустрии.


Это первая статья из цикла, посвящённого архитектурным паттернам в iOS разработке. Расскажу про плюсы и минусы, а также когда и где их лучше применять. В этой статье начну с основных и самых популярных MV(X) практик. Во второй части речь пойдёт о реализациях концепции Чистой Архитектуры (Clean Architecture), а в третьей — об FRP практиках и паттернах в iOS разработке.

Я Маша, ведущий инженер-разработчик iOS в КРОК, а также аспирант-препод в МЭИ.

Быть iOS-разработчиком непросто. Сначала Xcode не хочет подключаться к устройству, потом ты возишься неделю с сертификатами, потом полгода изучаешь Swift и пытаешься понять когда использовать `guard`, а когда — `if let`. Или когда ты уже написал не один калькулятор и приходишь на собеседование в крутой проект, тебя с порога начинают закидывать какими-то страшными аббревиатурами.

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

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

Что такое MV(X)?

Основные части MV(X) архитектур это, так или иначе, модели, виды и контроллеры.

Модель – отвечает за использование предметных (бизнес, domain) данных. Это так же может быть data access layer. Модели бывают двух видов: активные и пассивные. Активные модели умеют уведомлять окружающих об изменениях в себе, а пассивные — нет.

Вид (Представление, View) – отвечает за слой представления (GUI). В каком виде вам это представить? :) Вид не обязательно должен быть связан с UI отрисовкой: если вы хотите представлять данные в виде сменяющих друг друга диодов на arduino-плате — этим все равно будет заниматься Вид. Помимо представления пользователю данных, у Вида есть ещё одна важная задача: принять событие пользователя (нажатие на кнопку, например) и пнуть кого-то, кто знает что с этим делать.

Контроллер/Презентер/ViewModel – так или иначе отвечают за связь модели с контроллером. В основном занимаются тем, что пробрасывают события модели в вид, а события вида – в модель, соответствующим образом их преобразуя и обрабатывая. Например, изменяя модель в ответ на действия пользователя на экране (виде), или изменяют вид в ответ на изменения модели. Как правило являются пассивными участниками процесса и реагируют только на внешние стимулы (события от Вида или Модели).

Хорошая архитектура нужна чтобы:

Код становится читабельнее (как минимум для человека, знакомого с архитектурой, как максимум — для любого человека)

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

(проще и быстрее) покрыть код юнит-тестами

Расширять (масштабировать) код без мигрени и убийств

Хотя скорее всего если вы нашли эту статью, то вы уже знаете зачем вам нужна архитектура :)

Создание архитектуры программы или как проектировать табуретку (обязательно походите по ссылкам внутри)

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

CocoaMVC это iOS архитектура, которую предложили Apple, создавая iOS Developer SDK. Но стоит оговориться, что, вообще говоря, видение Apple немного отличается от традиционного понимания MVC как архитектуры.

Традиционная MVC выглядит как-то так:


Пунктирная стрелочка означает зависимость. То есть когда Контроллер владеет Видом, то Вид оказывается у него в зависимости (то есть у Контроллера есть поле типа Вид). У нас в свифте мы зависимость часто видим на примере `weak` свойств на родительские объекты и/или делегаты.

Вид: рисует кнопочки

зависит от Контроллера

сообщает Контроллеру о событиях ввода

запрашивает данные у Модели

получает изменения в данных от Модели

Контроллер: знает где взять текст для кнопочки и какой кнопочке его дать

может менять конфигурацию Вида или заменять один Вид другим

запрашивает изменения в данных Модели

Модель: знает как выглядят данные (типы, структуры), что делать с данными (бизнес-логика), где лежит БД и как в нее ходить (инкапсулирует доступ к данным)

зависит от Контроллера

меняет данные по запросу Контроллера

зависит от Вида

сообщает Виду об изменении в данных

Тут важный момент в том, что Вид не имеет состояний, то есть является stateless. Он просто отрисовывается заново, как только в Модели что-то меняется. Хороший пример: веб-страница в рунете нулевых, которая полностью перезагружается, если вы нажали на какую-то кнопку.

А ещё обратите внимание, что у View нет доступа на запись в Модель. Все изменения Модели производятся только через Контроллер, при этом права на чтение Модели у Вида не просто есть — он ими активно пользуется (чтобы обновлять себя и показывать актуальные данные на экране).

Вкратце достоинства классической MVC:

К одной Модели можно присоединить несколько Видов так, чтобы не менять код Модели. Например, табличные данные хочется показать и таблицей, и гистограммой, и пай-чартом.

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

Разработка бизнес-логики не зависит от разработки представлений и vice versa. А значит, можно посадить несколько человек одновременно долбить код одной фичи.

Однонаправленный поток данных (Вид -> Контроллер -> Модель -> Вид) делает дебаг проще и приятнее

Все это звучит очень классно, но немного запутанно и архаично. Apple предлагает вот такую реализацию Cocoa MVC:


Любой iOS-ник скажет, что эта картинка вообще не вяжется с реальностью и будет по-своему прав. На самом деле в большинстве случаев Cocoa MVC будет выглядеть вот так:


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

ViewController и View настолько плотно завязаны друг на друга, что сложно вообще отличить где заканчивается одно и начинается другое. С одной стороны: ну вот же UIView — его можно отдельным классом написать, там экстеншены-категории все вот это вот, вы меня за дурака держите?

С другой: UIView не умеет отрабатывать IBAction (тк он не посылает никаких actions — это делают UIControl), а .xib-ы легче подключаются к контроллерам (наверняка же сталкивались с историей, что если xib не заканчивается на -Controller то он не подключается “из коробки”? :)). В итоге чтобы отделить UIView от UIViewController в терминах целого экрана, а не отдельного компонента — нужно пройти сквозь огонь, воду и LLVM.

Так как ходить через LLVM никому не нравится, все пихают логику Вида в UIViewController, что приводит к тому, что MVC элегантным жестом превращается… в Massive View Controller. Разобраться в этой мешанине делегатов, датасорсов, экшенов и просто вспомогательных функций — задача на любителя.

Происходит это из-за того, что у Вида может быть достаточно большое количество различных состояний: представьте себе форму регистрации из серии “введите емейл, пароль, подтвердите пароль”. Логично, чтобы все поля имели валидацию ввода и цветовую индикацию: хорошо/плохо. Получается, что это три текстфилда, каждый с как минимум двумя состояниями. Записать это состояние в Вид нельзя — он stateless, в Контроллер тоже нельзя — он не должен управлять состояниями Вида, лишь передавать ему данные. В итоге, состояние Вида приходится хранить в Модели. Так у нас Модель получается перегружена: она хранит в себе и Domain Model (модель предметной области: данные, бизнес логика и тп) и View Model (модель вида: его состояния, хранимые данные, логика переключений состояний и тп).

Пример несколько утрированный, и опытные разработчики скорее всего не увидят проблему в реализации подобной фичи (ну сделай ты кастомный контрол и делов-то!). Но если говорить о формальном следовании архитектуре MVC/CocoaMVC, то видно как всё быстро становится достаточно запутанно.

Ну а протестировать взаимодействие между View и ViewController — задача настолько нетривиальная, что в MVC приложениях как правило тестируют только модель и нетворк слой (что во многих случаях бывает так же сложно разделить как и UIView с UIViewController: представьте модель данных и интерфейс доступа к данным (к БД)).


Прозорливый читатель заметит, что всех проблем c Cocoa MVC можно было бы избежать, если пару UView+UIViewController воспринимать как View, а под слой Controller выделить отдельную сущность, которая не знает о UIKit и занимается только тасканием данных и событий между View и Model, как и было задумано.

Не MVC единым: как применять MVVM в iOS

Сегодня мы будем рассматривать паттерн MVVM. Поговорим о его преимуществах по сравнению с MVC, а также рассмотрим один очень маленький пример и один достаточно большой, который в дальнейшем вы сможете применять в своей работе как образец хорошей архитектуры для практически любого проекта, использующего MVVM. Итак, начнем с основ :)

Основы

Напомню, что в MVC контроллер может общаться как с моделью (Model), так и с представлением (View). В MVVM мы имеем несколько другую схему, и ее очень важно запомнить: User → View → ViewController → ViewModel → Model. То есть, пользователь видит кнопку (View), нажимает на нее, а далее нагрузку берет на себя ViewController, выполняя какие-то действия с интерфейсом, например, меняет цвет этой кнопки. Далее, ViewModel посылает запрос серверу на получение данных, добавляет их в Model или выполняет какие-то другие действия с моделью.

Главное, что тут нужно запомнить: в MVVM у нас появился новый класс ViewModel, который сам общается с моделью, то есть мы сняли с контроллера эту обязанность и теперь контроллер занимается тем, чем надо — он работает с представлениями (View) и даже не знает о существовании модели.

Практика

Bond — несколько меньший фреймворк, он больше для новичков, но мы же с вами крутые спецы, зачем нам этот детский сад ;) Сегодня мы будем использовать RxSwift.

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

Простой пример MVVM в ReactiveX

Простой пример

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

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

Storyboard для простого примера MVVM

Единственный момент, чтобы при скролле наши картинки могли быть правильно отцентрированы, используем CollectionViewFlowLayoutCenterItem и ассоциируем его с классом UICollectionViewFlowLayoutCenterItem.swift (можете найти его в папке с проектом). Вот код на GitHub.

Так должен выглядеть наш Podfile:

RxCocoa — это extension для всех UIKit’овских элементов. То есть, мы можем написать: UIButton().rx_tap и получим ControlEvent , который является ObservableType . Представьте, что у нас есть объект UISearchBar . Обычно, не используя RxSwift, мы бы подписали наш контроллер как делегат и следили бы за изменением свойства text . Используя RxSwift, мы можем написать что-то в этом роде:

И теперь главное. Для нашей задачи мы не подписываем контроллер как делегат для UICollectionView , а делаем вот что:

В итоге, мы пишем меньше кода, код становится более читабельным, и если у нас нет данных (метод класса ViewModel.getData() вернет Observable ) то ничего и не произойдет, мы даже не запустим весь наш процесс.

Теперь подробней разберем метод класса ViewModel getData() . Если бы мы получали данные от сервера (что будет показано чуть позже), я бы добавил метод для получения данных, а так, использую private dataSource с картинками, который просто добавил в проект.

Тут мы создаем объект Observable и используем метод just , который говорит: вернуть последовательность, которая содержит только один элемент — массив элементов UIImage .

Также заметьте, что класс ViewModel является структурой. Сделано это для того, чтобы при использовании дополнительных свойств для этого класса у нас был уже готовый инициализатор.

Сложный пример

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

Когда работаете с последовательностями, в конце каждого вызова необходимо добавлять addDisposableTo(disposeBag) к объекту.

let disposeBag = DisposeBag() — в примере он объявлен как свойство. Это нужно, чтобы во время вызова системой deinit , происходило освобождение ресурсов для Observable -объектов.

Moya имеет расширение, написанное специально для RxSwift. Называется оно Moya/RxSwift (да, вот так банально и называется, а вы что думали?).

Начнем с Podfile’a:

Чтобы можно было работать с Moya, нам понадобится создать enum и подчинить его протоколу TargetType. В папке с проектом ReactX этот файл называется GithubEndpoint.swift . Мы будем использовать api для github. У нас будет всего четыре endpoint’a, но в своем проекте можете добавить столько, сколько вам нужно.

Private extension для String понадобится нам позже. Подчиняем наш GithubEndpoint протоколу TargetType.

Если вы используете методы, отличные от GET , как в нашем примере, то можете опять применить конструкцию switch.parameters — т. к. мы ничего не передаем, то просто возвращаем nil . Вы можете, используя switch , передавать дополнительную информацию, необходимую вашему серверу. sampleData — поскольку Moya работает с тестами, то эта переменная обязательна.

Приступим к нашему примеру. Так выглядит storyboard:

storyboard для сложного примера MVVM

Связываем элементы с нашим контроллером:

Добавим несколько свойств в нашем ViewController:

provider — это и есть Moya-объект, типом у которого выставлен наш enum .

latestRepositoryName — Observable . Каждый раз, когда пользователь начинает что-то писать в searchBar , мы следим за изменениями, точнее подписываемся на изменения. rx_text — из импортированного нами RxCocoa, категории для UIKit-элементов. Можете посмотреть сами на другие свойства.

Дальше мы фильтруем текст и используем только тот, в котором больше 2 символов.

distinctUntilChanged — проверяет предыдущий текст, если текст изменился, то он пропускает его дальше.

Идем дальше, создаем наш ViewModel(IssueTrackerModel) :

Вначале мы создали два метода: findRepository и findIssues . Первый будет возвращать optional Repository объект, второй — по такой же логике будет возвращать уже Observable[Issue] .

Метод mapObjectOptional() вернет optional -объект, в случае, если ничего не будет найдено, так же как и mapArrayOptional() вернет optional array . debug() — выведет debug-информацию на консоль.

Далее, метод trackIssues() объединяет эти два метода. Важным моментом тут является flatMapLatest() , который создает одну последовательность из другой. Его принципиальное отличие от flatMap() в том, что, когда flatMap() получает значение, он начинает длинную операцию (long task) и при получении следующего значения вначале доделывает предыдущую операцию. И это не совсем то, что нам нужно, т. к. пользователь уже может начать новый ввод текста. Нам нужно отменить предыдущую операцию и начать новую — для этого как раз и подходит flatMapLatest() .

Observable.just(nil) — просто вернет nil , который в дальнейшем будет заменен на пустой массив следующим методом. replaceNilWith([]) — заменит nil в пустом массиве.

Теперь нам нужно связать эти данные с UITableView . Помним, что нам не нужно подписываться на UITableViewDataSource , у RxSwift есть для этой цели метод rx_itemsWithCellFactory . Вот как выглядит метод setup() , находящийся во viewDidLoad() :

Еще один немаловажный момент — в каком потоке будут происходить операции. У нас есть два метода: subscribeOn() и observeOn() . В чем же разница между ними? subscribeOn() указывает, в каком потоке начать всю цепь событий, тогда как observeOn() — где начать следующую (см. картинку ниже).

Разница между subscribeOn и observeOn

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

Нужен MVP, разработка под iOS, Android или прототип приложения? Ознакомьтесь с нашим портфолио и сделайте заказ уже сегодня!

Model-View-Controller

MVC Schema

MVC состоит из трех компонент: View (представление, пользовательский интерфейс), Model (модель, ваша бизнес логика) и Controller (контроллер, содержит логику на изменение модели при определенных действиях пользователя, реализует Use Case). Основная идея этого паттерна в том, что и контроллер и представление зависят от модели, но модель никак не зависит от этих двух компонент. Это как раз и позволяет разрабатывать и тестировать модель, ничего не зная о представлениях и контроллерах. В идеале контроллер так же ничего не должен знать о представлении (хотя на практике это не всегда так), и в идеале для одного представления можно переключать контроллеры, а также один и тот же контроллер можно использовать для разных представлений (так, например, контроллер может зависеть от пользователя, который вошел в систему). Пользователь видит представление, на нем же производит какие-то действия, эти действия представление перенаправляет контроллеру и подписывается на изменение данной модели, контроллер в свою очередь производит определенные действия над моделью данных, представление получает последнее состояние модели и отображает ее пользователю.

Модель – отдельный класс, у которого есть методы получения данных (модель в реализациях часто включает в себя так же и Data Access Level):

Пример модели не самый удачный в данном случае, но все-таки не всегда бывает необходимость иметь действительно описанную бизнес модель в классах, иногда хватает и работы с DataSet'ами. Самое интересное, это реализация контроллера, по сути это code behind aspx страницы.

Данный подход даст нам возможность с легкостью написать тесты для модели, но не для контроллера (конечно же, все возможно, но придется постараться).

Model-View-Presenter

Хотя логика и слабая, но все же теперь она вся в презентере, и мы теперь можем тестировать отдельно SolutionPresenter вместе с ISolutionView, используя Mock’и. Более подробный пример присутствует в статье [2].

Model-View-ViewModel

Честно говоря, не знаю, используется ли данный паттерн где-то, кроме WPF и Silverlight. Здесь опять присутствуют три компоненты: модель, представление и третий компонент – дополнительная модель под названием ViewModel. Данный паттерн подходит к таким технологиям, где присутствует двухсторонний биндинг (синхронизация) элементов управления на модель, как в WPF. Отличие от MVP паттерна заключается в том, что свойство SelectedRecord, из предыдущего примера, должно находится не в представлении, а в контроллере (ViewModel), и оно должно синхронизироваться с необходимым полем в представлении. Как раз и выходит, что в этом и есть основная идея WPF. ViewModel – это некоторый суперконвертор, который преобразует данные модели в представление, в нем описываются основные свойства представления, а также логика взаимодействия с моделью. Рекомендую ознакомиться со статьей [3].

Литература

P.S. Надеюсь моя статья даст основное представление в понимании данных паттернов. Буду рад комментариям.

See Also

Found a misprint? Feel free to send a Pull Request or open an issue.

Have a question about the post? You tried, something does not work? GitHub discussions.

Читайте также: