Site icon AppTractor

MVVM: архитектурный шаблон для структурирования представлений SwiftUI

MVVM (Model-View-ViewModel) — это архитектурный паттерн для структурирования представлений. Цель паттерна — отделить определение представления от бизнес-логики, лежащей в его основе. Если все сделано правильно, ваши представления не будут зависеть от конкретного типа модели.

Хотя MVVM в основном использовался во времена UIKit/AppKit, он по-прежнему часто применяется в SwiftUI. По моему опыту, его использование не всегда последовательно и приводит скорее к шаблону View-ViewModel. Однако это может быть оправдано, если рассматривать его как «чрезмерную инженерию». Давайте посмотрим!

Что такое архитектурный паттерн MVVM?

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

Визуально паттерн выглядит следующим образом:

MVVM — распространенный архитектурный паттерн, используемый разработчиками в SwiftUI.

Паттерн состоит из трех слоев:

Для работы с MVVM не всегда нужно использовать модель. Модель также может быть уровнем доступа к данным, например, кэш контента на удаленном сервере. Чтобы эффективно использовать паттерн, мне нравится думать о нескольких важных правилах:

Обеспечение соблюдения этих правил в вашем коде автоматически приведет к созданию многократно используемого, тестируемого и изолированного кода.

Как использовать 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) — это архитектурный паттерн, который позволяет структурировать представления, слой данных и бизнес-логику. Это популярный паттерн, но используется он по-разному, в зависимости от предпочтений разработчиков. Следование строгим правилам делает ваш код более тестируемым, многократно используемым и простым для понимания.

Спасибо!

Источник

Exit mobile version