[Разработка под iOS, Разработка мобильных приложений] Архитектурные паттерны в iOS: страх и ненависть в диаграммах. MV(X)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Каждый iOS разработчик в своей жизни уходил с собеседования в расстроенных чувствах и мыслью «это что еще за новая аббревиатура?». Архитектурами пугают и джунов, и миддлов, и синьоров (и наверное даже синьорит). Важно не просто знать что стоит за названием, но еще и в каком случае какую использовать. Литературы по этому вопросу преступно мало, редкие обсуждения в интернете ограничиваются собственным опытом и какими-то поделками на гитхабе.В этом цикле из трёх статей я кратко разберу все популярные архитектурные паттерны, использующиеся в 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.MVCCocoaMVC — это iOS архитектура, которую предложили Apple, создавая iOS Developer SDK. Но стоит оговориться, что, вообще говоря, видение Apple немного отличается от традиционного понимания MVC как архитектуры.Традиционная MVC выглядит как-то так:
Сплошная стрелочка по заветам UML тут означает ассоциацию. Переводя на программерский: Контроллер владеет Моделью (например у класса Контроллера есть поле типа Модели). Я дальше вместо совершенно нетолерантного слова «владеть» буду говорить «Контроллер ЗНАЕТ О Модели», ведь знание — сила.Пунктирная стрелочка означает зависимость. То есть когда Контроллер владеет Видом, то Вид оказывается у него в зависимости (то есть у Контроллера есть поле типа Вид). У нас в свифте мы зависимость часто видим на примере `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: представьте модель данных и интерфейс доступа к данным (к БД)).В итоге, чтобы сделать действительно MVC-шный MVC на iOS придется исхитряться ломая либо Cocoa MVC (например давая UIView возможность самостоятельно ходить в модель), либо логику UIKit и делегирования (чтобы сообщения в контроллер приходили действительно от условных UIView, а не от других контролов и контроллеров).
MVPПрозорливый читатель заметит, что всех проблем c Cocoa MVC можно было бы избежать, если пару UView+UIViewController воспринимать как View, а под слой Controller выделить отдельную сущность, которая не знает о UIKit и занимается только тасканием данных и событий между View и Model, как и было задумано.
То есть:
- Презентер:
- зависит от Вида
- сообщает Виду о необходимости обновить отображение измененных данных
- знает о Модели
- запрашивает изменить данные
- получает сообщения об изменившихся данных
- Вид:
- знает о Презентере
- запрашивает у Презентера реакцию на действия пользователя («пользователь нажал на красную кнопку, куда его послать, сэр?»)
- Модель:
- зависит от Презентера
- получает запросы Презентера на изменения данных
- посылает Презентеру сообщения об изменившихся данных
Выглядит классно и вполне адекватно! Только вот…Нам очень хочется, чтобы Вид ничего не знал о Модели, так? Если собирать эту диаграмму в код напрямую, то получится, что в условном Виде содержится полем объект Контроллера, Контроллер содержит внутри себя Модель, ну и при инициализации Вида у нас по цепочке через Контроллер будет инициализироваться управляющий объект Модели. Так знает Вид о Модели или нет? Как-то некрасиво получается, да и с концепцией не вяжется.Эта проблема сборки (assembly problem) корнями растет напрямую из Cocoa MVC. Ведь именно Cocoa MVC заставляет нас в первую очередь ориентироваться на UIViewController как на единицу сборки и навигации. В идеальном мире мы бы сделали как:```let newModel = Model() // а возможно она уже где-то существует и мы на нее ссылаемсяlet newPresenter = Presenter(model: newModel)let newView = MyViewController(presenter: newPresenter)```Получается красивая инъекция зависимостей через конструктор, и все бы хорошо, если бы было понятно а где собственно она должна происходить. По идее (согласно Cocoa MVC) мы вызываем новые виды (UIViewController) из других видов (других UIViewController). В итоге получается что-то вроде `self.present(newViewController)`. Так значит какой-то другой Вид будет знать про модель нового Вида? Или этим должен заниматься Презентер? Если отдать эту честь Презентеру, то получится что мы создаем как-то шибко много зависимостей и таскаем данные туда-сюда: Вид: эй, Презентер, юзер хочет перейти на новый экран, что мне делать?Презентер: ну нужно НовыйВид показать. Вот ему модель, вот ему презентер, вот тебе собранный НовыйВид. Заменяй себя на него, а мы с тобой деинитмся… так, падажжи... В большинстве примеров в интернете и в жизни вы найдете код, где ViewController спокойно себе инитит модель, а потом кладет её в параметры инициализатора Презентера. У такого подхода нет ничего плохого, только надо следить за ссылками и понимать, что это не чистый MVP «как на диаграмме», да и заменить модель при случае будет не так просто, как хотелось бы. Решается проблема сборки как правило тем, что создается ещё один управляющий слой: Роутер (Router), который собирает MVP-кусочки, управляет их жизненным циклом и передает их друг другу при необходимости. По этой же причине так часто встречается связка MVP+Coordinator (на эту архитектуру часто ссылаются как на MVP+C).MVP+CВыглядит это дело вот так:
Задача координатора — создавать MVP блоки и управлять тем какой UIViewController показывается на экране прямо сейчас (например через `navigationController.pushViewController`). В этой схеме оказывается, что Координатор знает, какому Презентеру какую Модель подсунуть. Что в общем случае не ломает нашу MVP концепцию и вообще кажется довольно-таки логичным и удобным:Вид: эй, Презентер! Юзер Иванов хочет посмотреть на красную кнопку.Презентер: *потея* ох, но у нас её нету, надо искать где-то в другом месте… Координатор, нужно показать красную кнопку Иванову!Координатор: ага, ну понятно. Собираем, значит: ЭкранСКраснойКнопкой — есть, ПрезентерЭкранаСКраснойКнопкой ему вот, модель… модель для Иванова, ага… нашел, кладу в ПрезентерЭкранаСКраснойКнопкой. Собрано. Показываю ЭкранСКраснойКнопкой. Вас деиничу. Спасибо за содействие.Презентер: *деинитится вместе с Видом и, возможно, Моделью*
Это не единственный способ решить проблему сборки в MVP, но остальные методы не слишком-то элегантны:
Так что подведем итоги:
- При грамотном подходе все три компоненты (Вид, Презентер и Модель) растут примерно в одинаковых масштабах, что здорово помогает справиться с проблемой Massive View Controller.
- Все разделено настолько естественным образом, что масштабирование не представляет никаких угроз: все обязанности гарантированно разделены — масштабируем что хотим и где хотим.
- Покрыть тестами логику ответа на действия пользователя теперь тоже намного проще: Презентер хорошо отделён от Вида
- Правда, как это ни парадоксально, переиспользовать Презентер вряд ли получится: всё же, хоть он и отделен, но заточен на работу с конкретным представлением [2] Впрочем, тут все действительно зависит от вас и ваших навыков в SOLID
MVP оказывается хорошей альтернативой MVC, если выбран осознанно для решения конкретной проблемы. В противных случаях он ощущается как «слишком много лишнего кода» (намёк на роутер) и какой-то громоздкий костыль к MVC («нафига ещё презентер, если ViewController это всё умеет?»)MVP хорошо подходит небольшим командам, однако в крупном проекте с большим количеством разработчиков может стать настоящим яблоком раздора в GIT: разработчики могут постоянно править одни и те же переиспользуемые классы (например, роутеры или модели), работая над разными фичами, что неизменно приводит к большому количеству конфликтов при мержах, разгребать которые никому не нравится :)
Источники:[1] iOS Architecture Patterns and Best Practices for Advanced Programming - 2021[2] MVP vs. MVVM in iOS Development. Let's discuss those architectural… | by Ahmed Ragab Issa [3] iOS Architecture Patterns. Demystifying MVC, MVP, MVVM and VIPER | by Bohdan Orlov | iOS App Development MVVMMVVM очень похож на MVP, но совсем другой :) Он был создан в Microsoft для работы с WPF и Silverlight, но благодаря своей элегантной модуляризации приобрел популярность и далеко за пределами WPF приложений.Давайте начнём с картинки:
Как видно, картинка в точности повторяет MVP, только стрелочки почему-то двунаправленные стали, а Презентер превратился в Модель Вида (ViewModel). Ну и как читать это безобразие?Начнём с того, что тут View — это, как и в MVP, связка UIView+UIViewController. Мы вроде уже достаточно взрослые на данном этапе чтобы понять почему. Дальше, кстати, будет так же.Второе: двунаправленная стрелочка. Она означает биндинг (binding), или, по-русски, связывание данных. Вкратце это работает как в реакте: если значение связанной с лейблом переменной изменилось — на экране оно тоже тут же изменится без дополнительных телодвижений с нашей стороны. Стрелочка в обратную сторону означает, что если данные изменились в UI (например, юзер что-то ввел), то и в связанной переменной они тоже изменятся без дополнительных чтений из текстфилда. Всё это звучит очень круто и удобно, но потом приходит iOS-ник и такой:
И действительно, штатных (предоставленных Apple) средств организовать биндинг в iOS SDK (вне SwiftUI) нет, но зато есть другие инструменты, с помощью которых можно симулировать такое поведение.Так как делать биндинги вообще? Есть четыре способа:
- KVO (Key-Value Programming) — очень крутая и мощная штука прямиком из Cocoa, которая позволяет подписаться и наблюдать (observe) за переменной по её имени (keyPath) и получать уведомления об изменении её значения (value). Кажется, то что надо!
- FRP (Functional Reactive Programming) — парадигма программирования (заметьте: не язык и не фреимворк, а целая пАрАдИгМа) для обработки событий и данных как потоков. Ну, например представьте, что у вас есть какой-то поток данных и вы хотите для каждого объекта в этом потоке что-то предпринять. Эдакий forEach, только вы не знаете когда появится новый объект.```let lowercaseNames = cast.map { $0.lowercased() }``` видали? Вот вам и FRP, где поток — это статичный массив (cast).Реализация FRP в Swift обычно отдается на откуп RxSwift и ReactiveSwift, но есть ещё и Combine от Apple — сейчас можно говорить, что он лучше всех, но это конечно не точно.
- Delegate — делегирование можно использовать чтобы передавать сообщения об изменении данных.
- Boxing — если вы первым делом подумали про `didSet` — то это для вас! Похоже на KVO, кстати, но придется каждое свойство оборачивать в кастомный дженерик класс (Box). Подробнее детали реализации можно посмотреть в крутом туторе вот тут.Ну а если вы пришли из Objective-C, то с паттерном «коробки» у вас и подавно проблем возникнуть не должно! Это тот же самый паттерн, что используется в Obj-C (и не только) для оборачивания скалярных типов в ссылочные (например float в NSNumber), только теперь мы ещё и листенер туда навесить пытаемся. По сути — обычная обертка, совсем не страшно.
Пятый, бонусный способ: SwiftUI и его @State и @BindingУдивительное дело, но в SwiftUI Apple решила отказаться от концепции ViewController как такового! Вместо этого роль передатчика данных между View и Model на себя берет FRP фреймворк Combine (а вы думали отвертитесь от его изучения?). Самая простая иллюстрация для тех кто совсем в танке:- в описании View (читайте его как ViewModel) переменная помечается аннотацией @State- в описании Компонента (CounterButton, читайте это уже View) эта же переменная, привязываемая к UI контролу, помечается аннотацией @Binding - вуа-ля! Очень реактивно и роскошно :)
Тут есть конечно свои нюансы (например то, что SwiftUI больше подходит для Redux-like паттернов), но да ладно.Что же мы видим в итоге?
- Модель Вида:
- зависит от Вида
- биндится с данными в Виде, которые могут обновляться в Модели или в GUI
- знает о Модели
- запрашивает изменить данные
- получает сообщения об изменившихся данных
- Вид:
- знает о Модели Вида
- биндится с данными в Модели Вида, которые могут обновляться в Модели или в GUI
- Модель:
- зависит от Модели Вида
- получает запросы Модели Вида на изменения данных
- посылает Модели Вида сообщения об изменившихся данных
При этом функции компонент очень строго разделены и немного отличаются от оных в MVC/MVP:Модель:
- Содержит описание данных
- Реализует CRUD [2]
- Оповещает Модель Вида о произошедших изменениях в данных
- Содержит бизнес-логику [1]
- Содержит логику валидации данных [1]
- Вообще говоря MVVM никак не описывает реализацию Модели, и она может быть реализована любым способом, хоть VIPER модулем [3]
Вид:
- Определяет структуру, расположение и внешний вид элементов интерфейса (как в MVC и MVP)
- Содержит логику Вида: анимации, переходы между Видами и манипуляции с дочерними Видами
- Передаёт действия пользователя дальше (в Модель Вида)
Модель Вида:
- Хранит состояние Вида
- Знает о Модели и может менять её состояние (вызывая соответствующие методы Модели) [1]
- Как правило образует связь «один ко многим» с несколькими Моделями [3]
- Преобразует данные, полученные от Модели в формат, удобный для отображения Видом и обратно
- Ничего не знает о Виде и может с ним коммуницировать только благодаря биндингам
- Может содержать логику валидации данных [2]
- Может включать в себя бизнес-правила работы с действиями пользователя [2,3]
Как видим, Модель Вида — какой-то монстр, который всё обо всех знает и всеми погоняет. Это и достоинство, и недостаток MVVM:
- С одной стороны, Модель Вида — удобный медиатор, оторванный от мира UI и Data Access, а значит её удобно тестировать и можно переиспользовать на других платформах, если вдруг понадобилось сделать еще и приложение для tvOS, например
- с другой стороны, Модель Вида занимается и управлением состояниями своего Вида и зависимостей, и различного рода логикой. А значит её код разрастается и мы возвращаемся к проблеме Massive View Controller. Чтобы этого избежать, необходимо крайне осторожно подходить к проектированию Моделей Вида
И, хоть Модель Вида и легко протестировать, то дебажить приложение на MVVM совсем непросто: ведь благодаря биндингам данные меняются мгновенно. И, что бы это не значило со стороны пользователя, для разработчика это означает огроменный call stack подписок и ивентов, в котором чёрт ногу сломит:
Иллюстрация из [4]Впрочем, если у команды есть опыт реактивного программирования, то всё это не должно стать проблемой.MVVM хорошо подходит опытным командам среднего размера, которые работают над проектом с большим количеством представлений (экранов). Особенно хорошо MVVM себя показывает в проектах, где надо сильно отделить бизнес-логику от логики отрисовки представлений — такое часто встречается в кросс-платформенных приложениях, или приложениях, которым в будущем может понадобиться клиент-сателлит на другую платформу (tvOS, watchOS, ...).Из-за сильного разделения MVVM позволяет нескольким разработчикам работать над одной фичей одновременно, практически не пересекаясь в коде — это самая «простая» для понимания и внедрения архитектура, которая обладает таким замечательным свойством.
Источники:[1] iOS Architecture Patterns and Best Practices for Advanced Programming - 2021[2] Advanced iOS App Architecture | raywenderlich.com[3] Modern MVVM iOS App Architecture with Combine and SwiftUI [4] iOS Architecture Patterns. Demystifying MVC, MVP, MVVM and VIPER | by Bohdan Orlov | iOS App Development [5] Architectural Patterns by Pethuru Raj, Anupama Raman, Harihara Subramanian [6] MVP vs. MVVM in iOS Development. Let's discuss those architectural… | by Ahmed Ragab Issa В большинстве случаев этих трёх паттернов достаточно, чтобы написать классное приложение и поддерживать его на протяжении нескольких лет. Конечно, это не все MV(X) паттерны, встречающиеся в реальной жизни, литературе и собеседованиях. Но, зная это, уже не ударишь в грязь лицом на своём первом собеседовании или проекте :)На больших проектах со сложной бизнес-логикой требуются более основательные подходы к проектированию кодовой базы приложения. Один из таких подходов — концепция Чистой Архитектуры, о которой мы поговорим в следующей статье. Обсудим VIPER, CleanSwift, RIBs и относительно малоизвестную штуковину под названием Elements.Не переключайтесь! :)
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под iOS, Разработка мобильных приложений, Интерфейсы] Compositional Layout: стоит ли игра свеч?
- [Я пиарюсь, Разработка мобильных приложений, Flutter] Storybook + Flutter = storybook_flutter
- [Информационная безопасность, Разработка под iOS, Гаджеты, Смартфоны] Apple закрыла активно используемую уязвимость нулевого дня в iPhone, iPad и Watch
- [Разработка под iOS, Xcode, Swift] 7 Кругов SPM или как сделать модульное приложение на Swift Package Manager
- [Разработка под iOS, Разработка мобильных приложений, Swift, Дизайн мобильных приложений] iOS. UI. Приëмы. Часть 1
- [Разработка мобильных приложений, Разработка под Android, Kotlin] Android и привязка к жизненному циклу компонентов
- [Разработка мобильных приложений, Машинное обучение, Искусственный интеллект, Natural Language Processing] OpenAI: более 300 сторонних приложений работают на GPT-3
- [Разработка мобильных приложений, Git, Big Data, Машинное обучение] DVC — Git для данных на примере ML-проекта
- [Разработка мобильных приложений, Разработка под Android, Kotlin] Android + Redux = <3
- [Разработка мобильных приложений, Разработка под Android, DevOps, Gradle] Советы по работе с Gradle для Android-разработчиков
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_ios (разработка под ios), #_lichnyj_opyt (личный опыт), #_blog_kompanii_krok (
Блог компании КРОК
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:14
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Каждый iOS разработчик в своей жизни уходил с собеседования в расстроенных чувствах и мыслью «это что еще за новая аббревиатура?». Архитектурами пугают и джунов, и миддлов, и синьоров (и наверное даже синьорит). Важно не просто знать что стоит за названием, но еще и в каком случае какую использовать. Литературы по этому вопросу преступно мало, редкие обсуждения в интернете ограничиваются собственным опытом и какими-то поделками на гитхабе.В этом цикле из трёх статей я кратко разберу все популярные архитектурные паттерны, использующиеся в 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 – так или иначе отвечают за связь модели с контроллером. В основном занимаются тем, что пробрасывают события модели в вид, а события вида – в модель, соответствующим образом их преобразуя и обрабатывая. Например, изменяя модель в ответ на действия пользователя на экране (виде), или изменяют вид в ответ на изменения модели. Как правило являются пассивными участниками процесса и реагируют только на внешние стимулы (события от Вида или Модели).Любой нормальный человек, только что закончивший изучать ООП, задаст себе вопрос: а нафига плодить эти сущности? Зачем мне так много классов/структур/файлов/сообщений? Какие такие офигенные бенефиты дает вся эта возня? Вон я писал все в одном файле на Паскале/Питоне/Си/Свифт-плейграуде — и работало же!Хорошая архитектура нужна чтобы:
Сплошная стрелочка по заветам UML тут означает ассоциацию. Переводя на программерский: Контроллер владеет Моделью (например у класса Контроллера есть поле типа Модели). Я дальше вместо совершенно нетолерантного слова «владеть» буду говорить «Контроллер ЗНАЕТ О Модели», ведь знание — сила.Пунктирная стрелочка означает зависимость. То есть когда Контроллер владеет Видом, то Вид оказывается у него в зависимости (то есть у Контроллера есть поле типа Вид). У нас в свифте мы зависимость часто видим на примере `weak` свойств на родительские объекты и/или делегаты. То есть:
Видим знакомую картину: Контроллер — это промежуточный слой между Видом и Моделью. Он принимает события из Вида и Модели, и посылает запросы на модификации туда и туда. В Контроллер можно положить всю логику, которая не помещается в Модель: например преобразует данные для «красивого» отображения — это не часть бизнес-логики, так что в Модели ей не место. Любой 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: представьте модель данных и интерфейс доступа к данным (к БД)).В итоге, чтобы сделать действительно MVC-шный MVC на iOS придется исхитряться ломая либо Cocoa MVC (например давая UIView возможность самостоятельно ходить в модель), либо логику UIKit и делегирования (чтобы сообщения в контроллер приходили действительно от условных UIView, а не от других контролов и контроллеров). MVPПрозорливый читатель заметит, что всех проблем c Cocoa MVC можно было бы избежать, если пару UView+UIViewController воспринимать как View, а под слой Controller выделить отдельную сущность, которая не знает о UIKit и занимается только тасканием данных и событий между View и Model, как и было задумано. То есть:
Задача координатора — создавать MVP блоки и управлять тем какой UIViewController показывается на экране прямо сейчас (например через `navigationController.pushViewController`). В этой схеме оказывается, что Координатор знает, какому Презентеру какую Модель подсунуть. Что в общем случае не ломает нашу MVP концепцию и вообще кажется довольно-таки логичным и удобным:Вид: эй, Презентер! Юзер Иванов хочет посмотреть на красную кнопку.Презентер: *потея* ох, но у нас её нету, надо искать где-то в другом месте… Координатор, нужно показать красную кнопку Иванову!Координатор: ага, ну понятно. Собираем, значит: ЭкранСКраснойКнопкой — есть, ПрезентерЭкранаСКраснойКнопкой ему вот, модель… модель для Иванова, ага… нашел, кладу в ПрезентерЭкранаСКраснойКнопкой. Собрано. Показываю ЭкранСКраснойКнопкой. Вас деиничу. Спасибо за содействие.Презентер: *деинитится вместе с Видом и, возможно, Моделью* Это не единственный способ решить проблему сборки в MVP, но остальные методы не слишком-то элегантны: Так что подведем итоги:
Источники:[1] iOS Architecture Patterns and Best Practices for Advanced Programming - 2021[2] MVP vs. MVVM in iOS Development. Let's discuss those architectural… | by Ahmed Ragab Issa [3] iOS Architecture Patterns. Demystifying MVC, MVP, MVVM and VIPER | by Bohdan Orlov | iOS App Development MVVMMVVM очень похож на MVP, но совсем другой :) Он был создан в Microsoft для работы с WPF и Silverlight, но благодаря своей элегантной модуляризации приобрел популярность и далеко за пределами WPF приложений.Давайте начнём с картинки: Как видно, картинка в точности повторяет MVP, только стрелочки почему-то двунаправленные стали, а Презентер превратился в Модель Вида (ViewModel). Ну и как читать это безобразие?Начнём с того, что тут View — это, как и в MVP, связка UIView+UIViewController. Мы вроде уже достаточно взрослые на данном этапе чтобы понять почему. Дальше, кстати, будет так же.Второе: двунаправленная стрелочка. Она означает биндинг (binding), или, по-русски, связывание данных. Вкратце это работает как в реакте: если значение связанной с лейблом переменной изменилось — на экране оно тоже тут же изменится без дополнительных телодвижений с нашей стороны. Стрелочка в обратную сторону означает, что если данные изменились в UI (например, юзер что-то ввел), то и в связанной переменной они тоже изменятся без дополнительных чтений из текстфилда. Всё это звучит очень круто и удобно, но потом приходит iOS-ник и такой: И действительно, штатных (предоставленных Apple) средств организовать биндинг в iOS SDK (вне SwiftUI) нет, но зато есть другие инструменты, с помощью которых можно симулировать такое поведение.Так как делать биндинги вообще? Есть четыре способа:
Тут есть конечно свои нюансы (например то, что SwiftUI больше подходит для Redux-like паттернов), но да ладно.Что же мы видим в итоге?
Иллюстрация из [4]Впрочем, если у команды есть опыт реактивного программирования, то всё это не должно стать проблемой.MVVM хорошо подходит опытным командам среднего размера, которые работают над проектом с большим количеством представлений (экранов). Особенно хорошо MVVM себя показывает в проектах, где надо сильно отделить бизнес-логику от логики отрисовки представлений — такое часто встречается в кросс-платформенных приложениях, или приложениях, которым в будущем может понадобиться клиент-сателлит на другую платформу (tvOS, watchOS, ...).Из-за сильного разделения MVVM позволяет нескольким разработчикам работать над одной фичей одновременно, практически не пересекаясь в коде — это самая «простая» для понимания и внедрения архитектура, которая обладает таким замечательным свойством. Источники:[1] iOS Architecture Patterns and Best Practices for Advanced Programming - 2021[2] Advanced iOS App Architecture | raywenderlich.com[3] Modern MVVM iOS App Architecture with Combine and SwiftUI [4] iOS Architecture Patterns. Demystifying MVC, MVP, MVVM and VIPER | by Bohdan Orlov | iOS App Development [5] Architectural Patterns by Pethuru Raj, Anupama Raman, Harihara Subramanian [6] MVP vs. MVVM in iOS Development. Let's discuss those architectural… | by Ahmed Ragab Issa В большинстве случаев этих трёх паттернов достаточно, чтобы написать классное приложение и поддерживать его на протяжении нескольких лет. Конечно, это не все MV(X) паттерны, встречающиеся в реальной жизни, литературе и собеседованиях. Но, зная это, уже не ударишь в грязь лицом на своём первом собеседовании или проекте :)На больших проектах со сложной бизнес-логикой требуются более основательные подходы к проектированию кодовой базы приложения. Один из таких подходов — концепция Чистой Архитектуры, о которой мы поговорим в следующей статье. Обсудим VIPER, CleanSwift, RIBs и относительно малоизвестную штуковину под названием Elements.Не переключайтесь! :) =========== Источник: habr.com =========== Похожие новости:
Блог компании КРОК ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:14
Часовой пояс: UTC + 5