Архитектура 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. Тщательно изучив требования и сложность своего приложения, разработчики могут выбрать шаблон проектирования, который наилучшим образом соответствует их потребностям.