Архитектура VIPER, Swift, XCode, нетехническая.

Понимание архитектуры VIPER в Swift

Пройдемся по VIPER с нетехнической стороны

Понимание VIPER в Swift

В мире разработки для iOS существует множество архитектурных шаблонов на выбор. Одним из таких шаблонов, получивших популярность в последние годы, является архитектура VIPER. VIPER расшифровывается как View, Interactor, Presenter, Entity и Router и представляет собой модульный подход к созданию приложений iOS.

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

В этой статье мы углубимся в архитектуру VIPER в Swift, подробно рассмотрим каждый компонент и то, как они работают вместе, чтобы сделать приложение модульным, тестируемым и удобным в сопровождении.

Компоненты архитектуры VIPER

Вид

Представление отвечает за представление данных пользователю и обработку взаимодействия с пользователем. Он не имеет никакой логики, а вместо этого делегирует действия пользователя Presenter для обработки. Представление может быть UIViewController, UIView или даже UITableViewCell. Представление является пассивным, что означает, что оно ничего не знает о данных, логике или архитектуре приложения.

protocol ExampleView: AnyObject {
  func displayData(_ data: [String])
}

class ExampleViewController: UIViewController, ExampleView {
  var presenter: ExamplePresenter?
  func displayData(_ data: [String]) {
    // Update the UI with the data
  }
  @IBAction func buttonPressed(_ sender: UIButton) {
    presenter?.buttonPressed()
  }
}

Интерактор

Interactor отвечает за бизнес-логику и управление данными. Он содержит варианты использования приложения и предоставляет данные Presenter. Interactor взаимодействует с сущностями для выполнения таких операций, как CRUD (создание, чтение, обновление и удаление) над данными.

protocol ExampleInteractorInput {
  func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
  func dataFetched(_ data: [String])
}
class ExampleInteractor: ExampleInteractorInput {
  var output: ExampleInteractorOutput?
  var dataService: ExampleDataService?
  func fetchData() {
    dataService?.fetchData(completion: { [weak self] data in
      self?.output?.dataFetched(data)
    })
  }
}

Ведущий

Presenter отвечает за логику презентации. Он получает входные данные от View и Interactor, обрабатывает их, а затем обновляет View. Presenter знает о View, но не наоборот. Presenter также преобразует данные из Interactor в формат, который может быть представлен View.

protocol ExamplePresenter {
  func buttonPressed()
}

class ExamplePresenterImpl: ExamplePresenter {
  weak var view: ExampleView?
  var interactor: ExampleInteractorInput?
  func buttonPressed() {
    interactor?.fetchData()
  }
}
extension ExamplePresenterImpl: ExampleInteractorOutput {
  func dataFetched(_ data: [String]) {
    view?.displayData(data)
  }
}

Сущность

Entity представляет данные приложения. Это может быть база данных, сетевая служба или любой другой источник данных. Entity отвечает за операции CRUD над данными и взаимодействует с Interactor.

struct ExampleData {
  let title: String
  let subtitle: String
}

protocol ExampleDataService {
  func fetchData(completion: @escaping ([ExampleData]) -> Void)
}
class ExampleDataServiceImpl: ExampleDataService {
  func fetchData(completion: @escaping ([ExampleData]) -> Void) {
    // Fetch the data and call the completion handler with the data
  }
}

Маршрутизатор

Маршрутизатор отвечает за навигацию и маршрутизацию между различными модулями приложения. Он знает обо всех модулях приложения и отвечает за переходы между ними.

protocol ExampleRouter {
  func navigateToDetail()
}

class ExampleRouterImpl: ExampleRouter {
  weak var viewController: UIViewController?
  func navigateToDetail() {
    let detailViewController = DetailViewController()
    viewController?.navigationController?.pushViewController(detailViewController, animated: true)
  }
}

Работа VIPER Architecture

Каждый компонент архитектуры VIPER имеет четкую и определенную роль. Представление отправляет пользовательский ввод в Presenter, который, в свою очередь, связывается с Interactor для выполнения операций с данными. Затем Interactor возвращает результат Presenter, который преобразует его в формат, который может быть представлен View. Маршрутизатор отвечает за переход между различными модулями приложения.

Связь между каждым компонентом осуществляется через протоколы. Это упрощает написание модульных тестов для каждого компонента и гарантирует, что их можно заменить или изменить, не затрагивая другие компоненты.

Преимущества архитектуры VIPER

Модульная конструкция

Архитектура VIPER имеет модульную структуру, что упрощает ее обслуживание и масштабирование. У каждого компонента есть четкая и определенная роль, что позволяет легко добавлять или удалять функции, не затрагивая другие компоненты.

// Before adding new functionality
class ExampleInteractor: ExampleInteractorInput {
  var output: ExampleInteractorOutput?
  var dataService: ExampleDataService?

  func fetchData() {
    dataService?.fetchData(completion: { [weak self] data in
      self?.output?.dataFetched(data)
    })
  }
}
// After adding new functionality
class ExampleInteractor: ExampleInteractorInput {
  var output: ExampleInteractorOutput?
  var dataService: ExampleDataService?
  var analyticsService: ExampleAnalyticsService?
  func fetchData() {
    dataService?.fetchData(completion: { [weak self] data in
      self?.output?.dataFetched(data)
      analyticsService?.trackEvent("Data Fetched")
    })
  }
}

Тестируемость

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

class ExamplePresenterTests: XCTestCase {
  var sut: ExamplePresenterImpl!
  var mockView: ExampleViewMock!
  var mockInteractor: ExampleInteractorInputMock!

  override func setUp() {
    super.setUp()
    sut = ExamplePresenterImpl()
    mockView = ExampleViewMock()
    mockInteractor = ExampleInteractorInputMock()
    sut.view = mockView
    sut.interactor = mockInteractor
  }
  func test_buttonPressed() {
    sut.buttonPressed()
    XCTAssertTrue(mockInteractor.fetchDataCalled)
  }
}

Разделение ответственности

Архитектура VIPER следует принципу разделения задач. Каждый компонент имеет четкую и определенную роль, что упрощает поддержку и модификацию приложения.

// Before modifying the View
class ExampleViewController: UIViewController, ExampleView {
  var presenter: ExamplePresenter?

  func displayData(_ data: [String]) {
    // Update the UI with the data
  }
}
// After modifying the View
class ExampleViewController: UIViewController, ExampleView {
  var presenter: ExamplePresenter?
  var data: [String] = []
  override func viewDidLoad() {
    super.viewDidLoad()
    presenter?.viewDidLoad()
  }
  func updateUI() {
    // Update the UI with the data
  }
}

Недостатки архитектуры VIPER

Сложность

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

protocol ExampleView: AnyObject {
  func displayData(_ data: [String])
}

class ExampleViewController: UIViewController, ExampleView {
  var presenter: ExamplePresenter?
  func displayData(_ data: [String]) {
    // Update the UI with the data
  }
  @IBAction func buttonPressed(_ sender: UIButton) {
    presenter?.buttonPressed()
  }
}

protocol ExampleInteractorInput {
  func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
  func dataFetched(_ data: [String])
}

class ExampleInteractor: ExampleInteractorInput {
  var output: ExampleInteractorOutput?
  var dataService: ExampleDataService?
  func fetchData() {
    dataService?.fetchData(completion: { [weak self] data in
      self?.output?.dataFetched(data)
    })
  }
}

protocol ExamplePresenter {
  func viewDidLoad()
  func buttonPressed()
}

class ExamplePresenterImpl: ExamplePresenter {
  weak var view: ExampleView?
  var interactor: ExampleInteractorInput?
  func viewDidLoad() {
    interactor?.fetchData()
  }
  func buttonPressed() {
    interactor?.fetchData()
  }
}

extension ExamplePresenterImpl: ExampleInteractorOutput {
  func dataFetched(_ data: [String]) {
    view?.displayData(data)
  }
}

protocol ExampleDataService {
  func fetchData(completion: @escaping ([String]) -> Void)
}

class ExampleDataServiceImpl: ExampleDataService {
  func fetchData(completion: @escaping ([String]) -> Void) {
    // Fetch the data and call the completion handler with the data
  }
}

Крутая кривая обучения

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

protocol ExampleView: AnyObject {
  func displayData(_ data: [String])
}

protocol ExampleInteractorInput {
  func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
  func dataFetched(_ data: [String])
}

protocol ExamplePresenter {
  func viewDidLoad()
  func buttonPressed()
}

protocol ExampleRouter {
  func navigateToDetail()
}

class ExampleViewController: UIViewController, ExampleView {
  var presenter: ExamplePresenter?
  func displayData(_ data: [String]) {
    // Update the UI with the data
  }

  @IBAction func buttonPressed(_ sender: UIButton) {
    presenter?.buttonPressed()
  }
}

class ExampleInteractor: ExampleInteractorInput {
  var output: ExampleInteractorOutput?
  var dataService: ExampleDataService?
  func fetchData() {
    dataService?.fetchData(completion: { [weak self] data in
      self?.output?.dataFetched(data)
    })
  }
}

class ExamplePresenterImpl: ExamplePresenter {
  weak var view: ExampleView?
  var interactor: ExampleInteractorInput?
  var router: ExampleRouter?
  func viewDidLoad() {
    interactor?.fetchData()
  }

  func buttonPressed() {
    interactor?.fetchData()
    router?.navigateToDetail()
  }
}

extension ExamplePresenterImpl: ExampleInteractorOutput {
  func dataFetched(_ data: [String]) {
    view?.displayData(data)
  }
}

class ExampleRouterImpl: ExampleRouter {
  weak var viewController: UIViewController?
  func navigateToDetail() {
    let detailViewController = DetailViewController()
    viewController?.navigationController?.pushViewController(detailViewController, animated: true)
  }
}

protocol ExampleDataService {
  func fetchData(completion: @escaping ([String]) -> Void)
}

class ExampleDataServiceImpl: ExampleDataService {
  func fetchData(completion: @escaping ([String]) -> Void) {
    // Fetch the data and call the completion handler with the data
  }
}

Заключение

В заключение, архитектура VIPER обеспечивает четкую и определенную структуру, упрощающую создание сложных приложений для iOS. Он поощряет модульность, тестируемость и разделение задач, делая кодовую базу более удобной в сопровождении, масштабируемой и простой в работе. Кроме того, он идеально подходит для больших команд разработчиков, поскольку снижает риск конфликтов и поощряет сотрудничество.

Однако архитектура VIPER может быть сложной и требовать много времени для настройки, что может быть непросто для небольших или простых приложений. Использование протоколов также может быть сложным для разработчиков, которые плохо знакомы с разработкой для iOS или не знакомы с объектно-ориентированным программированием. Кроме того, архитектура VIPER может быть перепроектирована для небольших или простых приложений, что делает ее ненужной.

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