MVVM (Model-View-ViewModel) — это архитектурный паттерн для структурирования представлений. Цель паттерна — отделить определение представления от бизнес-логики, лежащей в его основе. Если все сделано правильно, ваши представления не будут зависеть от конкретного типа модели.
Хотя MVVM в основном использовался во времена UIKit/AppKit, он по-прежнему часто применяется в SwiftUI. По моему опыту, его использование не всегда последовательно и приводит скорее к шаблону View-ViewModel. Однако это может быть оправдано, если рассматривать его как «чрезмерную инженерию». Давайте посмотрим!
Что такое архитектурный паттерн MVVM?
MVVM — это аббревиатура от Model-View-ViewModel, и изначально он был придуман архитекторами Microsoft. Разработчики используют его для разделения задач, гарантируя, что представление не привязано к определенной модели данных. Это приводит к созданию многократно используемого кода, поскольку вы можете использовать представление с любой моделью, если между ними есть модель представления, обеспечивающая коммуникационный слой.
Визуально паттерн выглядит следующим образом:
Паттерн состоит из трех слоев:
- Представление (View)
Это определение представления. В SwiftUI это будет декларативное определение представления. - Модель представления (ViewModel)
Представление напрямую биндится со свойствами модели представления для отправки и получения обновлений. Поскольку модель представления не имеет ссылок на представление, она становится многоразовой для использования с несколькими представлениями. - Модель (Model)
Модель относится к домену модели. Например, уContactView
будет модельContactViewModel
, которая действует как уровень связи с доменной модельюContact
.
Для работы с MVVM не всегда нужно использовать модель. Модель также может быть уровнем доступа к данным, например, кэш контента на удаленном сервере. Чтобы эффективно использовать паттерн, мне нравится думать о нескольких важных правилах:
- Представление взаимодействует только с ViewModel.
- Представление ничего не знает о (доменной) модели, лежащей за ViewModel.
- ViewModel не имеет ссылок на View и становится многоразовой для использования с любым View.
Обеспечение соблюдения этих правил в вашем коде автоматически приведет к созданию многократно используемого, тестируемого и изолированного кода.
Как использовать MVVM в SwiftUI
Теперь, когда мы знаем, как работает MVVM и что он обозначает, пришло время применить его на практике. Ранее мы уже обсуждали представление контактов, поэтому давайте используем его в этом примере.
Без использования MVVM ваш ContactView
мог бы выглядеть следующим образом:
struct Contact { let name: String } struct ContactView: View { let contact: Contact var body: some View { VStack { Text("Name: \(contact.name)") Button("Delete", action: deleteContact) } } func deleteContact() { // Perform contact deletion } }
На этом этапе код может быть в полном порядке. Пока его еще не так много, и бизнес-логики тоже не слишком много. Однако ContactView
строго связан с моделью Contact
и содержит бизнес-логику для удаления контактов. Ее также нелегко протестировать или повторно использовать с другими подобными типами.
Мы можем переписать эту логику, используя архитектурный паттерн MVVM. Для изящной миграции мы начнем с того, что подключим метаданные только к новому типу ContactViewModel
:
struct ContactViewModel { /// A public accessor to the contact's name. /// Implementors don't know the name is coming from a `Contact` type. var name: String { contact.name } /// Keep a reference to the (domain) model so we can perform any actions like deleting. private let contact: Contact /// The ViewModel only references the `Contact` model, but has no link to the view that's using the ViewModel. init(contact: Contact) { self.contact = contact } }
Теперь мы можем обновить наш ContactView
, чтобы использовать эту новую ViewModel:
struct ContactView: View { let viewModel: ContactViewModel var body: some View { VStack { Text("Name: \(viewModel.name)") Button("Delete", action: deleteContact) } } func deleteContact() { // Perform contact deletion } }
Это уже большое улучшение, поскольку мы убрали прямую связь между моделью и представлением.
Извлечение бизнес-логики из представления
Следующий шаг — извлечение бизнес-логики из представления. Честно говоря, многие разработчики используют для этого разные реализации. Я предпочитаю писать логику удаления внутри ViewModel, но паттерн изначально описывает реализацию удаления внутри модели Contact. Еще лучше было бы использовать здесь паттерн Репозиторий, чтобы вынести бизнес-логику в один ответственный тип, в результате чего ViewModel станет лишь коммуникационным слоем.
Я оставляю на ваше усмотрение, что вам больше нравится, но в этой статье я собираюсь перенести бизнес-логику удаления в ViewModel. Это уже большое улучшение, поскольку оно удаляет бизнес-логику из представления.
struct ContactView: View { let viewModel: ContactViewModel var body: some View { VStack { Text("Name: \(viewModel.name)") Button("Delete", action: viewModel.deleteContact) } } } struct ContactViewModel { /// ... func deleteContact() { // Perform contact deletion } }
Окончательная реализация ContactView
больше не связана с конкретной моделью или бизнес-логикой. Она взаимодействует только с нашей ViewModel, которая выступает в качестве коммуникационного слоя между представлением и моделью.
Использование протоколов для повышения возможности повторного использования представлений в MVVM
Реализация MVVM сделала наши представления более простыми и менее зависимыми, но мы можем сделать еще один шаг вперед, используя протоколы. До сих пор нам приходилось работать только со структурой Contact
, но может оказаться, что в вашем проекте потребуется другой тип контакта, например RemoteContact
.
Используя протоколы, вы позволите своим представлениям стать более гибкими. Для этого мы начнем с создания протокола ContactViewModel
:
protocol ContactViewModel { var name: String { get } func deleteContact() }
Нам не нужно обновлять наше представление, поскольку оно уже взаимодействует с тем же именем типа, но нам нужно переименовать нашу оригинальную модель ContactViewModel
в LocalContactViewModel
. Она также должна соответствовать протоколу ContactViewModel
:
struct LocalContactViewModel: ContactViewModel { var name: String { contact.name } private let contact: Contact init(contact: Contact) { self.contact = contact } func deleteContact() { // Perform contact deletion **locally**. } }
Наконец, мы можем приступить к определению новой модели RemoteContactViewModel
, которая будет выступать в качестве коммуникационного слоя между ContactView
и RemoteContact
:
struct RemoteContact { let name: String } struct RemoteContactViewModel: ContactViewModel { var name: String { contact.name } private let contact: RemoteContact init(contact: RemoteContact) { self.contact = contact } func deleteContact() { // Perform contact deletion **remotely**. // Potentially using a network request. } }
Преимущество использования подобных протоколов заключается в том, что они позволяют инстанцировать один и тот же ContactView
с несколькими типами ViewModel, которые взаимодействуют с различными (доменными) моделями:
let localViewModel = LocalContactViewModel(contact: Contact(name: "Antoine")) let remoteViewModel = RemoteContactViewModel(contact: RemoteContact(name: "Antoine")) /// We use the same `ContactView`, but different `ContactViewModel` types. let localContactView = ContactView(viewModel: localViewModel) let remoteContactView = ContactView(viewModel: remoteViewModel)
Мы создали многократно используемый и тестируемый код и настроились на масштабируемость в будущем.
Чрезмерная инженерия: всегда ли я должен использовать модели представлений?
Пример, приведенный в этой статье, возможно, заставил вас задуматься о чрезмерной инженерии: стоит ли использовать MVVM для таких простых представлений, как это?
Это справедливый вопрос, и на него можно ответить по-разному. Я предпочитаю начинать с малого и оптимизировать по мере необходимости. Вы можете сначала определить простой ContactView
и перенести его на новый MVVM-слой, когда ваш проект вырастет. Однако если вы ожидаете, что ваш проект будет расти, или хотите создать последовательную реализацию в рамках всего проекта, возможно, вам стоит использовать MVVM с самого начала.
Также важно принимать во внимание коллег, работающих над тем же проектом. Благодаря повторному использованию схожих паттернов в рамках всего проекта код становится более понятным. Даже те коллеги, которые не работали над ContactView, легко поймут код, поскольку узнают паттерн MVVM из других мест в проекте.
Заключение
MVVM (Model-View-ViewModel) — это архитектурный паттерн, который позволяет структурировать представления, слой данных и бизнес-логику. Это популярный паттерн, но используется он по-разному, в зависимости от предпочтений разработчиков. Следование строгим правилам делает ваш код более тестируемым, многократно используемым и простым для понимания.
Спасибо!