Привет, Хабр! Меня зовут Кирилл М��канков, я iOS-разработчик в ПСБ.
Сегодня хочу поговорить про особенности реализации MVVM. Не с практической, а с теоретической стороны. С практической стороны этот архитектурный шаблон давно уже изучен вдоль и поперёк. А вот теоретических исследований, особенно применимых к Swift (и в общем, и к SwiftUI в частности), не так уж и много. Давайте вместе попробуем закрыть этот пробел и обосновать с теоретической точки зрения те или иные вариации реализаций в нашей ежедневной работе. Мобильным разработчикам на ObjC, Java и Kotlin данное исследование и обсуждение тоже будет полезно. Присоединяйтесь!
Если вы уже хорошо знакомы с MVVM, то в конце приведен опрос об используемой лично вами реализации. Давайте вместе определим, какая из реализаций самая популярная.
Почему MVVM?
Но сначала объясню, почему именно MVVM.
Тут всё очень просто!
Для начала, это самый популярный шаблон проектирования по версии Мобиус от осени 2025 года в российском сообществе мобильных разработчиков. Ниже привожу выдержку результатов их опроса.

Дальше я с помощью ИИ исследовал популярность архитектур по данным из открытых источников. И узнал, что MVVM упоминается в 68% вакансий, связанных с мобильной разработкой. Солидно!
Этим шаблоном мы и сами активно пользуемся в нашей iOS-разработке, а наши коллеги из Android-разработки тоже начали активно его внедрять.
Решено! – Пристегнитесь! – Поехали исследовать!
Что такое MVVM?
Сначала давайте определимся с терминами. Что же такое MVVM?
Ответ на этот вопрос и прост, и сложен одновременно. Все, наверняка, читали множество различных определений и видели вот такую картинку с Википедии, как на рис. 1?

Но что можно понять по этой схеме? Что означают эти квадратики? Это классы? Это объекты? Это целые группы, чем-то связанные? Что означают сплошные стрелочки, а что пунктирная? Что означает цветовая маркировка? По одной лишь схеме разобраться сложно.
Необходимо читать описание и детально разбираться, что я и предлагаю сделать.
Слоистая архитектура
Из описания шаблона в той же Википедии, мы узнаем, что MVVM – это слоистая или многоуровневая архитектура (ссылка на вики на английском, на русском). Отлично! Значит квадратики на схеме – это не классы и не объекты, это целые слои классов/объектов или даже протоколов (мы увидим это в нескольких примерах далее), логически связанных между собой! Запоминаем, нам это пригодится дальше.
Цветовая индикация, похоже, в этом свете не означает ничего кроме того, что все три слоя не связаны логически друг с другом (Low coupling).
ViewModel
Перейдем сразу к смыслу и, возможно, структуре промежуточного слоя, называемого вью-моделью, ибо на нём будет сфокусировано все наше дальнейшее исследование, т. к. с реализациями уровня View и ViewModel обычно проблем не возникает, там все довольно однозначно. Все нижеперечисленное также взято из описания шаблона проектирования MVVM в Википедии на английском языке, т. к. русский вариант сильно беднее:
Итак, вью-модель – это:
В обязательном порядке абстракция! Это объясняется необходимостью развязать слой представления (вью) от слоя бизнес-логики (модели), сделать их независящими друг от друга.
Преобразователь данных из формата, в котором они лежат в модели, к формату в котором они отображаются на вьюхе. И возможно, но не обязательно – в обратную сторону.
И, наконец, это может быть так называемый binder, связывающий напрямую значения вью-модели со значениями вью и обратно. Ответственность binder’а не обязательна в шаблоне MVVM, но без неё придется писать слишком много одинакового boilerplate-кода. Binder как раз и решает проблему этого одинакового boilerplate-кода. Без binder’а MVVM превращается в MVP (или в MVC с пассивной моделью).
Вот те 3 ответственности, которые должны и могут присутствовать во вью-модели.
При этом, как мы отметили выше, эти 3 ответственности не обязаны сосредотачиваться в одном классе и/или объекте – они могут быть разнесены по разным классам/объектам в пределах одного уровня архитектуры. Далее в примерах мы увидим, что так оно и есть…
Чтобы разобраться правильно ли мы реализуем вью-модель, необходимо разобрат��ся, как можно реализовать каждую из ответственностей.
С биндером и преобразователем данных обычно проблем возникает меньше всего. В рамках этой статьи правильность их реализации мы рассматривать не будем, хоть и косвенно затронем преобразование данных, т. к. показать примеры без него довольно сложно. Вместо этого сконцентрируемся на способах реализации именно абстракции, т. к. это самый сложный вопрос. Если вы хотели бы разобраться в типичных ошибках реализации преобразователя данных и биндера, пишите в комментариях, подготовлю такое же детальное продолжение этой статьи.
Абстракция
Все дальнейшие примеры реализации абстракции я буду показывать на примере SwiftUI-вьюхи с таким телом (само тело дальше приводить не буду, чтобы не нагружать примеры кода):
var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text(viewModel.text) } .padding() }
Установку вью-модели во вью делаю прямо в самой вью для упрощения примеров. В реальном коде так делать не рекомендую, т. к. это создает сильную связанность и невозможность подмены, что противоречит самой сущности шаблона MVVM. В реальном коде связь должна происходить инъекциями за пределами самой вью, иначе абстракции становятся конкретикой! Однако, рассмотрение способов инъекций (фабрики, DI-фреймворки и т. д.) выходит за рамки этой статьи. Если вам они интересны, оставляйте комментарии под статьей, попробую провести и для них такой же детальный анализ.
5 способов реализации абстракции
Абстракцию на Swift (но при желании вы можете провести аналог��чный анализ для своего любимого языка) можно реализовать следующими 5 способами (упорядочу их для удобства от простого к сложному):
Способ 1. Протоколы (они же основное средство абстракции) и расширения этими протоколами. В такой реализации вью-модель (целый слой) – это просто протокол, что не позволяет использовать хранимые во вью-модели свойства. Реализация самая простая из всех рассмотренных:
struct ContentView: View { @State var viewModel: ViewModel = Model() ... } // реализация абстракции вью-модели на протоколе! protocol ViewModel { var text: String { get } } // реализация конкретной модели struct Model { let myText = "Hello, protocol world!" } // связывание вью-модели и модели, заставляем вью-модель преобразовывать данные extension Model: ViewModel { var text: String { myText } }
Способ 2. Наследование классов. В Swift нет чистых абстрактных классов, поэтому эмуляцию можно сделать двумя способами:
Вариант 2.1. Снова на протоколах, но дефолтная реализация методов приводит к падению приложения при их вызове. Не рекомендуется так делать, т. к. ошибки компилятора выключаются и будут отловлены только при исполнении программы. К тому же количество кода для выполнения той же задачи увеличивается на пустом месте. Сравните с примером из п.1:
struct AbstractProtocolView: View { @State var viewModel: AbstractProtocolViewModel = AbstractProtocolModel() ... } // Протокол заменяет абстрактный класс protocol AbstractProtocolViewModel { var text: String { get } } // Дефолтная реализация протокола эмулирует абстрактный метод extension AbstractProtocolViewModel { var text: String { fatalError("Abstract!") } } // реализация конкретной модели struct AbstractProtocolModel { let myText = "Hello, abstract protocol world!" } // связывание вью-модели и модели, заставляем вью-модель преобразовывать данные extension AbstractProtocolModel: AbstractProtocolViewModel { var text: String { myText } }
Вариант 2.2. Напрямую создавать классы с методами, приводящими к падению приложения при их вызове. Не рекомендуется так делать, т. к. это тоже сделает невозможным проверки компилятором, а ошибки будут отловлены только в момент исполнения:
struct ClassView: View { @State var viewModel: AbstractViewModel = ClassViewModel(model:ClassModel()) ... } // Эмуляция абстрактного класса class AbstractViewModel { var text: String { fatalError("Abstract!") } } // реализация абстракции вью-модели на абстрактном классе! class ClassViewModel: AbstractViewModel { override var text: String { model.myText } var model: ClassModel init(model: ClassModel) { self.model = model } } // реализация конкретной модели struct ClassModel { let myText = "Hello, class world!" }
Способ 3. Дженерики (Generics). Ограничения в них могут быть только протоколами или классами. Поэтому если вам нужно работать с моделями-структурами, то придется дополнительно абстрагировать и их, закрывая протоколами (количество абстракций при этом увеличивается):
struct GenericView: View { @State var viewModel: GenericViewModel = GenericViewModel(model: GenericModel()) ... } // реализация абстракции вью-модели на дженериках! class GenericViewModel<Model: GenericModel> { var text: String { model.myText } var model: Model init(model: Model) { self.model = model } } // реализация конкретной модели, ограничения в дженериках не могут быть структурами, только классами! class GenericModel { let myText = "Hello, generic world!" }
Способ 4. Ассоциированные типы (associatedtype). Как и дженерики, обязаны задавать ограничения протоколами или классами, поэтому тоже не могут использовать структуры-модели. Код получается еще более монструозным для такой простой задачи:
struct AssociatedTypeView: View { @State var viewModel: any AssociatedViewModel = MyAssociatedViewModel(model: AssociatedModel()) ... } // Абстракция на протоколе с ассоциированным типом protocol AssociatedViewModel { associatedtype Model: AssociatedModel var model: Model { get } var text: String { get } } // Связываем вью-модель и модель, заставляя преобразовывать данные extension AssociatedViewModel { var text: String { model.myText } } // реализация конкретной вью-модели class MyAssociatedViewModel: AssociatedViewModel { typealias Model = AssociatedModel var model: AssociatedModel var text: String { model.myText } init(model: AssociatedModel) { self.model = model } } // реализация конкретной модели class AssociatedModel { let myText = "Hello, associated type world!" }
Способ 5. Обертки, стирающие тип (Стирание типа, Type erasure). Малоизвестный способ создания абстракции. Но это самая часто встречающаяся реализация вью-модели. Просто посмотрите на этот код, сравните со своим и проголосуйте в опросе, похож ли этот код на ваш):
struct TypeErasureView: View { @State var viewModel: TypeErasureViewModel = TypeErasureViewModel(model: TypeErasureModel()) ... } // Вью-модель через стирание типа struct TypeErasureViewModel { let model: MyTextContainable init(model: MyTextContainable) { self.model = model } } // связывание вью-модели и модели extension TypeErasureViewModel { var text: String { model.myText } } // сокрытие доступа к модели за протоколом protocol MyTextContainable { var myText: String { get } } // реализация конкретной модели struct TypeErasureModel { let myText = "Hello, type erasure world!" } // прячем модель за абстракцию extension TypeErasureModel: MyTextContainable { }
Знаете ли вы ещё какие-нибудь варианты реализации абстракции на Swift? — Пишите в комментариях.
А сейчас сведем все рассмотренные способы в таблицу, отметив их сильные и слабые стороны:

Красным в таблице отмечены «западающие» свойства. Неудивительно, что при этом самыми распространённым оказывается способ создания абстракции через обертку, стирающую типы. Но самым простым оказывается малоизвестный способ на протоколах с дефолтными реализациями. Пройдите, пожалуйста, опрос в конце статьи, чтобы подтвердить или опровергнуть моё предположение.
Отмечу также, что все из этих способов создания абстракций можно преобразовывать друг в друга довольно легко. Например, эмуляцию абстрактных классов на протоколах (вариант 2.1) можно получить из реализации абстракции на протоколах с дефолтной реализацией (вариант 1, прошу прощения читателя за очень похожие названия, но отличия в реализациях действительно незначительны!), просто добавив к реализации протокола дефолтную реализацию метода, эмулирующего падение приложения при его вызове. Соответственно, чтобы произвести обратное преобразование из варианта 2.1 в вариант 1, необходимо удалить эмуляцию абстракции из дефолтной реализации протокола.
Далее из эмуляции абстрактных классов на протоколах (вариант 2.1) уже можно получить прямую эмуляцию абстрактных классов (вариант 2.2), просто преобразовав протокол с дефолтной реализацией в класс с абстрактным методом и создав его конкретный потомок.
Зная эту информацию, вы теперь можете реализовывать вью-модель с помощью любого из указанных способов и при необходимости менять её с одного способа на другой, учитывая преимущества каждого способа!
А теперь следующий вопрос: «Существуют ли еще какие-то способы создания абстракций?»
Абстракция с помощью шаблонов проектирования
Оказывается, существуют. Один из способов создания абстракции с помощью поведенческого шаблона «Посредник» («Mediator») из книги Банды Четырех указан прямо в описании шаблона MVVM в Википедии: "The viewmodel may implement a mediator pattern".
Mediator + Strategy + Observer
Внимательно ознакомимся с шаблоном и убедимся, что главная его цель – это как раз сделать коммуникацию между несколькими объектами, которые не должны знать друг о друге, что и нужно вью-модели в шаблоне MVVM. При этом связь должна быть инкапсулирована и иметь возможность быть легко замененной на любую другую независимо от самих коммуницирующих объектов! Теоретически с использованием этого шаблона мы можем менять реализацию медиатора на любую другую, ни вью, ни модель при этом не должны измениться ни на символ кода.
Однако в чистом виде, описываемом в шаблоне, инкапсулируется связь о взаимодействии однотипных объектов. В более сложном случае, когда как в MVVM нам нужно инкапсулировать взаимодействие между разнотипной вью и разнотипной моделью, к шаблону «Посредник» приходится добавлять шаблон «Стратегия» («Strategy») (и даже комбинацию шаблонов «Команда»(«Command») и «Цепочка обязанностей»(«Chain of Responsibility»)) и «Observer» («Наблюдатель») из той же книги Банды Четырех, которые как раз и абстрагируют взаимодействие со вью («Стратегия», «Команда» и «Цепочка обязанностей») и с моделью («Наблюдатель»).
Помните табличные делегаты? Вот это как раз реализация шаблона «Стратегия», позволяющая одну и ту же табличку (UITableView) использовать с различными данными. А помните механизм target-action? Это пример реализации «Команды» и «Цепочка обязанностей» — в простейшем случае у разработчиков не вызывает проблем с переиспользованием этих механизмов на простейших экранах.
Примером шаблона «Наблюдатель» является стек CoreData, уведомляющий и обновляющий свои NSManagedObject’ы. При этом NSFetchedResultsController — это пример инкапсуляции конкретного взаимодействия любой таблички с CoreData — нам не нужно каждый раз писать новые взаимодействия, если мы хотим отображать данные из базы в табличном виде, но при желании мы легко можем реализовать свой новый контроллер. Но вот такие сложные взаимодействия у разработчиков уже вызывают проблемы, хоть сам NSFetchedResultsController и довольно прост.
Скомбинировав указанные 3 шаблона вместе, мы получим схему MVC с пассивной моделью с рис. 7.2 из документации Apple (см. рис. 2).

Т. е. MVVM без binder’а может вырождаться не только в MVP, но и в MVC в зависимости от направления зависимости от вью к промежуточному слою, либо обратно (это, кажется, единственно отличие MVP от MVC с пассивной моделью или вы можете указать какие-то еще?).
Если теперь добавить в эту схему binder и перенаправить уведомления от модели с вью-модели (контроллера) напрямую во вью, то мы получим схему MVC с активной моделью, как на рис. 7.1 из той же документации Apple (см. рис. 3).

Реализуете ли вы MVVM по схеме MVC с активной/пассивной моделью? Если нет, то почему?
Есть ли еще какие-то способы реализации абстракции, чтобы на ее основе реализовать вью-модель?
«Adapter» («Адаптер»)
Конечно! Аналогом «Посредника» для реализации абстракции является шаблон «Адаптер»(«Adapter»). В книге Банды Четырех он описан в двух реализациях: как адаптер класса (с множественным наследованием) и адаптер объекта. Если мы попробуем глубже разобраться с обеими реализациями, мы с удивлением обнаружим, что адаптер классов с множественным наследованием – наши уже упомянутые реализации абстракции на Swift под вариантами 1 и 2.1, а адаптер объектов – это реализация варианта 5.
Внесем варианты на основе шаблонов проектирования в нашу таблицу. Расширенная версия указана в таблице 2.

Удобство тестирования
Что касается тестирования всех указанных вариантов реализации MVVM, это не должно составлять труда ни в одном из перечисленных вариантов, т. к.в основе всего шаблона MVVM лежит принцип абстракции и снижения связности между вью и моделью. Если в каком-то из вариантов у вас встречаются сложности, пишите в комментариях, попробуем разобраться вместе! В частности, трудности обычно встречаются с вариантом обертки, стирающей типы (он же «адаптер объекта»). Более подробно это будет рассмотренно ниже в разделе ошибочных реализаций.
Ошибочные реализации
Выше мы детально рассмотрели и попытались обосновать целых 6 различных способов реализации шаблона MVVM на языке Swift. Самое время переходить к рассмотрению ошибочных способов.
И начнем мы с самого очевидного!
Отсутствие абстракции во вью-модели
Ну, тут совсем все понятно. Код самый простой из всех рассмотренных. Однако, если отсутствует абстракция, разрушается весь смысл шаблона MVVM, то это уже не MVVM. В таком случае невозможно разрабатывать представление и бизнес-логику независимо друг от друга. Любое изменение, предложенное дизайнерами, непременно будет требовать изменений бизнес-логики. Любое изменение, предложенное владельцем продукта или пользователями, непременно будет требовать изменений в дизайне. Сущий ад для разработчика!
Если вам по каким-то причинам все же потребовалось убрать абстракцию из вью-модели, то лучше пересмотрите всю архитектуру конкретного экрана - очень вероятно, что MVVM к нему просто не применим.
Пример кода:
struct SpecificView: View { @State var viewModel: SpecificViewModel = SpecificViewModel(model: SpecificModel()) ... } // конкретная вью-модель с конкретной моделью, // ошибка: отсутствие абстракции! struct SpecificViewModel { var model: SpecificModel var text: String { model.myText } } // реализация модели struct SpecificModel { let myText = "Hello, specific world!" }
Абстракция на абстракции и абстракцией погоняет
Противоположный пример. Для правильной работы шаблона MVVM (чтобы отделить представление от логики) достаточно одной абстракции на уровне вью-модели. Но частенько у разработчиков, не пытавшихся разобраться в теории, возникают сложности с тестированием. Они пытаются тестировать вью-модель отдельно от модели на уровне модульных (юнит-) тестов. Но не понимая, как работает уменьшение связности конкретного выбранного способа реализации абстрактности, добавляют дополнительные способы, которые и используют для подмены реальных моделей на моки.
В результате такого ошибочного решения появляется примерно такой код:
struct MultiAbstractionView: View { @State var viewModel: MultiAbstractionViewModelProtocol = MultiAbstractionViewModel(model: MultiAbstractionModel()) ... } // протокол вью-модели protocol MultiAbstractionViewModelProtocol { var text: String { get } } // АБСТРАКЦИЯ вью-модели по схеме обертки, стирающей типы struct MultiAbstractionViewModel { var model: MultiAbstractionModelProtocol init(model: MultiAbstractionModelProtocol) { self.model = model } } // добавление еще одной АБСТРАКЦИИ через протокол к вью-модели! // и связывание вью-модели с моделью extension MultiAbstractionViewModel: MultiAbstractionViewModelProtocol { var text: String { model.myText } } // протокол модели protocol MultiAbstractionModelProtocol { var myText: String { get } } // сама модель struct MultiAbstractionModel { var myText = "Hello, crazy multi abstraction world!" } // добавление АБСТРАКЦИИ уже к модели, по сути просто дублирование интерфейса модели в интерфейсе протокола, чтобы отделить вью-модель от модели. extension MultiAbstractionModel: MultiAbstractionModelProtocol { }
Просто безумное количество кода для такого простого примера. Его даже больше, чем в крайне сложных реализациях на дженериках или с ассоциативным типом. Возможно, что больше только в реализации MVVM с медиатором и то только из-за того, что в неё добавлены шаблоны «Стратегия» и «Наблюдатель».
Если мы внимательно изучим этот код, то заметим, что он содержит сразу 3!!! абстракции:
И вью-модель скрывается за протоколом;
И модель скрывается за протоколом;
Еще и вью-модель реализуется по схеме обертки, стирающей типы.
Встречаются также реализации с двумя любыми реализациями абстракции из указанных трех.
В любом случае, это избыточно там, где хватит всего одной абстракции по правильным образцам, представленным выше. Пожалуйста, не реализовывайте MVVM так. Очевидно, что из трёх абстракций достаточно оставить лишь одну в нужном месте.
Приглашаю обсудить в комментариях, в каких случаях такая реализация имеет право на жизнь.
Вывод
В рамках детального анализа архитектуры MVVM удалось определить как минимум 6 различных способов реализации.
Самым популярным является способ с оберткой, стирающей типы модели. Он же «адаптер объекта».
Самым простым, но непопулярным является реализация на протоколах. Она же «адаптер класса». Видимо, непопулярность обусловлена тем, что вью-модель в такой реализации не может иметь хранимых свойств, что вызывает определенные сложности, хоть и ошибочно с точки зрения архитектуры, т. к. данные все же должны храниться в свойствах на уровне модели. Тем не менее мы настоятельно рекомендуем этот вариант к использованию.
Самым презираемым в сообществе, но при этом самым гибким, является способ реализации на «Посреднике» («Mediator»). Это, видимо, вызвано тем, что этот способ содержит в себе сразу как минимум 2 связанных шаблона («Стратегию» и «Наблюдателя») и очень сложен в правильной реализации. Также сложности может вызывать то, что в зависимости от деталей реализации этот шаблон может превращаться в различные архитектурные схемы (MVP, MVC с активной или пассивной моделью).
Остальные способы реализации совсем редкие и должны использоваться исключительно в случаях, когда раскрываются их сильные стороны.
Мы так же рассмотрели ошибочные реализации, чтобы каждый мог скорректировать свой подход.
Спасибо всем, кому было интересно, кто дочитал эту простыню до конца. Теперь предлагаю проголосовать, чтобы определить, а как же реализует MVVM сообщество и обсудить детали в комментариях.
