Введение в MVP, MVC, MVVM и VIPER. Что у них общего и в чем различия?
Вы все делаете с помощью MVC, а получается некрасиво? Не уверены, стоит ли переходить на MVVM? Слышали о VIPER, но не уверены, стоит ли оно того?
В этой статье я кратко рассмотрю некоторые популярные архитектурные шаблоны в среде iOS и сравню их в теории и на практике.
Более подробную информацию вы найдете, перейдя по ссылкам, указанным в тексте.
Освоение паттернов может вызвать привыкание, поэтому будьте осторожны:
Возможно, в конечном итоге вы зададите себе больше вопросов, чем до прочтения этой статьи, например:
— Кто должен владеть сетевыми запросами: Модель или Контроллер?
- Как передать модель в ViewModel нового представления?
— Кто создаёт новый модуль VIPER: Router или Presenter?
Почему стоит заботиться о выборе архитектуры?
Потому что если вы этого не сделаете, то однажды, отлаживая огромный класс с десятками разных методов и свойств, вы не сможете найти и исправить в нем ошибки.Естественно, такой класс сложно держать в голове целиком, поэтому вы всегда упустите из виду какие-то важные детали.
Если вы уже находитесь в такой ситуации, то весьма вероятно, что:
- этот класс является потомком UIViewController;
- данные сохраняются непосредственно в UIViewController;
- Подклассы UIView ни за что не отвечают;
- Модель — это просто контейнер для данных;
- вы не проводите модульные тесты.
Какао MVC , так что не расстраивайтесь.
С «яблоком» MVC не все в порядке, но мы вернемся к этому позже.
Теперь давайте определим знаки хорошая архитектура:
- сбалансированный распределение обязанности между организациями с жесткими ролями;
- проверяемость .
Обычно следует из первых признаков (не паникуйте, это легко осуществимо при соответствующей архитектуре);
- простота использования и низкие затраты на техническое обслуживание.
Почему распространение?
Распределение снижает нагрузку на мозг, когда мы пытаемся разобраться, как работает та или иная сущность.Если вы думаете, что чем больше вы развиваетесь, тем лучше ваш мозг адаптируется к пониманию сложных концепций, вы правы.
Но всему есть предел, и он достигается довольно быстро.
Таким образом, самый простой способ уменьшить сложность — разделить обязанности между несколькими субъектами в соответствии с принцип единой ответственности .
Почему тестируемость?
Тестируемость архитектуры определяет, насколько легко нам будет писать модульные тесты, а чаще всего, сможем ли мы их написать в принципе.Стоит ли вообще тестировать? Как правило, это вопрос не к тем, у кого есть упал модульные тесты после добавления нового функционала или после рефакторинга некоторых тонкостей класса.
Это означает, что тесты сохранено разработчикам от обнаружения проблемы во время выполнения.
Что может случиться с приложением, уже находящимся на устройстве пользователей, и исправить это возможно только неделю спустя .
Почему простота использования?
Здесь все понятно, но стоит отметить, что лучший код — это код, который никогда не был написан.И чем меньше у вас кода, тем меньше ошибок.
Поэтому желание писать меньше кода не означает, что разработчик ленив.
И при выборе самого разумного решения всегда следует учитывать стоимость его поддержки.
Основы МВ(Х)
Сегодня у нас есть множество вариантов шаблонов архитектурного проектирования: Первые три из них предполагают отнесение сущностей приложения к одной из 3 категорий:- Модели - лица, ответственные за данные предметной области или уровень доступа к данным, который манипулирует данными, например класс Человек или PersonDataProvider ;
- Взгляды - ответственные за уровень презентации ( графический интерфейс ); для среды iOS это все, что начинается с префикса пользовательский интерфейс ;
- Контроллер/Презентатор/ViewModel - посредник между Модель И Вид ; в целом ответственен за изменения Модель , реагируя на действия пользователя, выполняемые на Вид и обновления Вид используя изменения из Модель .
- понять их лучше;
- повторно использовать их (в основном применимо к Вид И Модель );
- тестируйте их отдельно друг от друга.
MVC
Как это было раньше
Прежде чем обсуждать видение Apple MVC, давайте посмотрим на традиционная версия .
В традиционном MVC Вид не хранит состояние в себе.
Контроллер просто "рендерит" Вид когда изменения Модель .
Например, веб-страница полностью перезагружается после того, как вы нажимаете ссылку, чтобы перейти куда-то еще.
Хотя можно реализовать традиционный MVC в среде iOS, это не имеет особого смысла из-за архитектурной проблемы: все три сущности тесно связаны, каждая сущность знает о двух других.
Это значительно снижает возможность повторного использования каждого элемента.
По этой причине мы даже не будем пытаться написать канонический пример MVC. Традиционный MVC кажется неприменимым для современной разработки под iOS.
MVC от Apple
Ожидания
Контроллер является посредником между Вид И Модель , следовательно, последние двое не знают о существовании друг друга.
Вот почему Контроллер сложно использовать повторно, но в принципе нас это устраивает, поскольку у нас должно быть место для этой сложной бизнес-логики, которая не вписывается в Модель .
В теории всё выглядит очень просто, но чувствуешь, что что-то не так, да? Вы, наверное, слышали, как люди расшифровывают MVC как Массивный контроллер просмотра .
Кроме, выгрузка ViewController стала важной темой для разработчиков iOS. Почему это происходит, если Apple просто взяла традиционный MVC и немного его улучшила?
Реальность
Cocoa MVC призывает вас писать Массивный Просмотрите контроллер, потому что контроллер очень вовлечен в жизненный цикл.
Вид , что сложно сказать, что он является отдельной сущностью.
Хотя у вас по-прежнему есть возможность перенести часть бизнес-логики и преобразования данных в Модель когда дело доходит до доставки работы в Вид , у вас не так много вариантов.
В большинстве случаев вся ответственность Вид заключается в отправке действий контроллеру.
В конечном итоге все заканчивается тем, что View Controller становится делегатом и источником данных, а также местом запуска и отмены запросов к серверу и вообще чего-либо еще.
Сколько раз вы видели этот код:
Вид -ячейка настраивается непосредственно с Модель .var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)
Это нарушает принципы MVC, но такой код можно увидеть очень часто, и, как правило, люди не понимают, что это неправильно.
Если вы строго следуете MVC, вам следует настроить ячейку внутри контроллера, а не передавать Модель в представлении, что увеличит Контроллер даже больше.
Cocoa MVC справедливо расшифровывается как Massive View Controller. Проблема не очевидна, пока она не доходит до модульные тесты (Надеюсь, что в вашем проекте это всё-таки пройдёт).
Поскольку View Controller тесно связан с Вид , становится сложно тестировать, и приходится идти изощренным путем, заменяя Вид Макетные объекты и моделирование их жизненного цикла, а также написание кода контроллера представления таким образом, чтобы бизнес-логика была максимально отделена от кода макета представления.
Давайте посмотрим на простой пример с детской площадки: import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
class GreetingViewController : UIViewController { // View + Controller
var person: Person!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .
TouchUpInside)
}
func didTapButton(button: UIButton) {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
Сборку MVC можно выполнить в «представляющем» контроллере представления.
Кажется, сложно проверить, не так ли? Мы можем выделить генерацию приветствий в новый класс ПриветствиеМодель и протестируем отдельно, но проверить логику представления (хотя в примере ее не так уж и много) внутри мы не можем ПриветствиеViewController без вызова методов жизненного цикла Вид напрямую ( просмотрDidLoad, DidTapButton ), что может привести к загрузке всех UIView, а это плохо для модульных тестов.
На самом деле тестирование UIViews на одном симуляторе (например, iPhone 4S) не гарантирует, что он будет нормально работать на других устройствах (например, iPad), поэтому я рекомендую снять галочку.
Хост-приложение из целевой конфигурации модульного теста и запустите их на симуляторе, не включая само приложение.
Взаимодействие между Вид И Контроллер Фактически не особенно поддается тестированию с использованием модульных тестов .
С учетом всего вышесказанного может показаться, что Cocoa MVC — довольно плохой выбор шаблона.
Но давайте оценим это с точки зрения признаки хорошей архитектуры , определенный в начале статьи:
- распределение : Вид И Модель фактически разделены, но Вид И Контроллер тесно связаны;
- проверяемость : из-за плохого распространения вы, вероятно, будете только тестировать Модель ;
- простота использования : Наименьшее количество кода среди других шаблонов.
Кроме того, он выглядит понятным, поэтому его легко сможет поддерживать даже неопытный разработчик.
Cocoa MVC — лучший архитектурный шаблон с точки зрения скорости разработки.
MVP
Реализация обещаний Cocoa MVC
Разве это не похоже на Apple MVC? На самом деле очень много, и зовут его MVP (вариант с пассивным Вид ).
Но означает ли это, что MVC от Apple на самом деле является MVP? Нет, это не так, потому что, как вы помните, там Вид И Контроллер тесно связаны между собой, тогда как посредником в MVP является Ведущий - не имеет никакого отношения к жизненному циклу View Controller. Вид можно легко заменить Макетные объекты , поэтому в Ведущий кода макета нет, но он отвечает за обновление Вид в соответствии с новыми данными и состоянием.
- А что, если я тебе это скажу? UIViewController - Этот Вид .С точки зрения MVP, подклассы UIViewController действительно существуют. Вид , но нет Ведущий .
Эта разница обеспечивает превосходную тестируемость, но за это приходится жертвовать скоростью разработки, поскольку вам приходится вручную точно связывать данные и события между Вид И Ведущий , как видно на примере ниже.
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
protocol GreetingViewPresenter {
init(view: GreetingView, person: Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter {
unowned let view: GreetingView
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
func showGreeting() {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var presenter: GreetingViewPresenter!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .
TouchUpInside)
}
func didTapButton(button: UIButton) {
self.presenter.showGreeting()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
Важное примечание относительно сборки
MVP — первый паттерн, выявляющий проблему сборки, возникающую из-за наличия трёх Действительно отдельные слои.Поскольку нам не нужно Вид знал о Модель , выполните сборку в представленном контроллере представления (который на самом деле Вид ) неверно, поэтому это нужно сделать в другом месте.
Например, вы можете создать сервис Маршрутизатор , который будет отвечать за сборку и презентацию Просмотр-просмотр .
Эта проблема возникает не только в MVP, ее также необходимо решать в все последующие шаблоны .
Давайте посмотрим на признаки хорошей архитектуры для MVP:
- распределение : Большая часть ответственности распределяется между Ведущий И Модель , А Вид ничего не делать;
- проверяемость : отлично, большую часть бизнес-логики мы можем протестировать благодаря простою View;
- простота использования : В нашем невероятно простом примере объём кода в два раза больше, чем в MVC, но при этом идея MVP очень проста.
MVP
С «блэкджеком» и «привязками»
Существует еще один вариант MVP — MVP с контролирующим контроллером.Он включает в себя прямую ссылку Вид И Модель , пока Ведущий (контролирующий контроллер) по-прежнему обрабатывает действия с Вид и способен изменить его.
Но, как мы узнали ранее, расплывчатое разделение ответственности само по себе плохо, как и тесная связь между Вид И Модель .
Но я не вижу смысла писать пример плохой архитектуры.
МВВМ
Самый новый из видов MV(X).
МВВМ является новейшим из шаблонов MV(X), поэтому будем надеяться, что он учитывает все проблемы, присущие MV(X).
Теоретически Модель-Представление-ViewModel выглядит очень хорошо.
Вид И Модель нам уже знакомы, например Посмотреть модель в качестве посредника.
Это очень похоже на MVP:
- MVVM рассматривает контроллер представления как Вид ;
- нет тесной связи между Вид И Модель .
Так что же это Посмотреть модель в среде iOS? Это независимый из представления UIKit Вид и ее состояние.
Посмотреть модель вызывает изменения в Модель и обновляется из уже обновленного Модель .
А поскольку привязка происходит между Вид И Посмотреть модель , то и первый соответственно обновляется.
Привязки
Я упоминал о них еще в разделе о MVP, но давайте взглянем на них поближе.Привязки доступны «из коробки» для разработки OS X, но недоступны разработчикам iOS. КВО и Уведомления, конечно, у нас есть, но они не так удобны, как привязки.
Поэтому при условии, что мы не хотим их писать сами, мы можем выбрать:
- одна из библиотек привязки на основе KVO (например, RZDataBinding или СвифтБонд );
- полноразмерный каркас для функциональное реактивное программирование , такой как РеактивныйКакао , RxSwift или ОбещаниеКит .
Когда пишешь, очень легко что-то сломать.
реактивно .
Другими словами, если что-то пойдет не так, вы можете потратить много времени на отладку приложения.
Стоит просто взглянуть на этот стек вызовов.
В нашем простом примере реактивный фреймворк или даже КВО — это излишество.
Мы будем напрямую спрашивать Посмотреть модель чтобы он обновлялся с помощью метода шоуПриветствие и используйте простое свойство для функции обратного вызова приветствиеСделалИзменить узнать об изменениях.
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
init(person: Person)
func showGreeting()
}
class GreetingViewModel : GreetingViewModelProtocol {
let person: Person
var greeting: String? {
didSet {
self.greetingDidChange?(self)
}
}
var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
required init(person: Person) {
self.person = person
}
func showGreeting() {
self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
}
}
class GreetingViewController : UIViewController {
var viewModel: GreetingViewModelProtocol! {
didSet {
self.viewModel.greetingDidChange = { [unowned self] viewModel in
self.greetingLabel.text = viewModel.greeting
}
}
}
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .
TouchUpInside)
}
// layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
И давайте снова вернемся к нашей оценке.
признаки хорошей архитектуры :
- распределение : Из нашего крохотного примера не понятно, но на самом деле в MVVM Вид имеет больше обязанностей, чем Вид из МВП.
Потому что первый обновляет свое состояние с помощью модели представления, устанавливая привязки, а второй направляет все события в Presenter и не обновляется сам (это делает Presenter);
- проверяемость : Модель представления ничего не знает о представлении, что позволяет нам легко его протестировать.
Представление тоже можно протестировать, но поскольку оно зависит от UIKit, то это можно просто пропустить;
- простота использования : тот же объем кода, что и в нашем примере MVP, но в реальном приложении, где вам придется маршрутизировать все события из представления в презентатор и обновлять представление вручную, MVVM будет намного тоньше (если вы используете привязки).
Однако тестируемость по-прежнему находится на хорошем уровне.
VIPER
Опыт строительства из кубиков Lego перенесен на дизайн iOS-приложений
VIPER Это наш последний кандидат, который особенно интересен, поскольку не относится к категории MV(X).К настоящему моменту вы должны согласиться, что разделение ответственности — это очень хорошо.
VIPER делает еще один шаг к разделению обязанностей и вместо привычных трех уровней предлагает пять .
- Интерактор содержит бизнес-логику, связанную с данными ( Сущности ): например, создание новых экземпляров сущностей или получение их с сервера.
Для этих целей вы будете использовать некоторые Службы и Менеджеры, которые рассматриваются как внешние зависимости, а не как часть модуля VIPER.
- Ведущий содержит бизнес-логику, связанную с пользовательским интерфейсом (но независимую от UIKit), вызывает методы в Интерактор .
- Сущности — простые объекты данных не являются уровнем доступа к данным, поскольку за это отвечает уровень Интерактор .
- Маршрутизатор отвечает за переходы между VIPER- модули .
Вам решать, насколько маленькими будут ваши кубики Лего.
Если мы сравним VIPER с паттернами типа MV(X), то увидим несколько отличий в распределении обязанностей:
- логика из Модель (взаимодействие данных) переходит в Интерактор , Есть также Сущности - структуры данных, которые ничего не делают;
- от Контроллер , Ведущий , ViewModel Обязанности по представлению пользовательского интерфейса перенесены в Ведущий , но без возможности изменения данных;
- VIPER это первый шаблон, который пытается решить проблему навигации, для этого есть Маршрутизатор .
import UIKit
struct Person { // Entity (usually more complex e.g. NSManagedObject)
let firstName: String
let lastName: String
}
struct GreetingData { // Transport data structure (not Entity)
let greeting: String
let subject: String
}
protocol GreetingProvider {
func provideGreetingData()
}
protocol GreetingOutput: class {
func receiveGreetingData(greetingData: GreetingData)
}
class GreetingInteractor : GreetingProvider {
weak var output: GreetingOutput!
func provideGreetingData() {
let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer
let subject = person.firstName + " " + person.lastName
let greeting = GreetingData(greeting: "Hello", subject: subject)
self.output.receiveGreetingData(greeting)
}
}
protocol GreetingViewEventHandler {
func didTapShowGreetingButton()
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
weak var view: GreetingView!
var greetingProvider: GreetingProvider!
func didTapShowGreetingButton() {
self.greetingProvider.provideGreetingData()
}
func receiveGreetingData(greetingData: GreetingData) {
let greeting = greetingData.greeting + " " + greetingData.subject
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var eventHandler: GreetingViewEventHandler!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .
TouchUpInside)
}
func didTapButton(button: UIButton) {
self.eventHandler.didTapShowGreetingButton()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of VIPER module, without Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
И все же вернемся еще раз к знаки .
- Распределение .
Без сомнения, VIPER является лидером в распределении ответственности.
- Тестируемость .
Здесь нет ничего удивительного: лучшее распространение означает лучшее тестирование.
- Простота использования .
Как вы уже догадались, первые два преимущества связаны со стоимостью поддержки.
Вам придется написать огромное количество интерфейсов для классов с небольшими обязанностями.
Так что там с Лего?
При использовании VIPER у вас может возникнуть ощущение, будто вы строите Эмпайр-стейт-билдинг из кубиков Lego, и это говорит о том, что у вас есть существует проблема .Возможно, вы слишком рано взялись за VIPER и вам следует подумать о чем-то более простом.
Некоторые игнорируют это и продолжают стрелять воробьев из пушки.
Я предполагаю, что они верят, что их приложения выиграют от VIPER когда-нибудь в будущем, даже если стоимость обслуживания сейчас непомерно высока.
Если вы считаете, что оно того стоит, то я рекомендую вам попробовать.
Генерамба — инструмент для генерации скелетов VIPER. Хотя лично я считаю, что это сродни использованию автоматический прицел вместо этого стрелять из того же пистолета рогатки .
Заключение
Мы рассмотрели несколько архитектурных шаблонов, и я надеюсь, что вы нашли ответы на некоторые из своих вопросов.Я не сомневаюсь, что ты это понял не существует «серебряной пули» среди шаблонов, а выбор архитектуры — это вопрос взвешивания компромиссов в вашей конкретной ситуации.
Мне кажется вполне естественным объединить несколько архитектур в одном приложении.
Например, вы начали с MVC, но после того, как поняли, что конкретный экран (вариант использования) стало слишком сложно поддерживать с помощью MVC, вы перешли на MVVM, но только для этого конкретного экрана.
Потому что на самом деле нет необходимости рефакторить другие экраны, для которых MVC работает нормально, тем более что обе архитектуры легко совместимы.
Сделайте это как можно проще, но не проще.Доступна английская версия Здесь .(с) Альберт Эйнштейн
Слайды, которые я представил на NSLondon, доступны.
Здесь .
Богдан Орлов, iOS-разработчик в Badoo Теги: #iOS #архитектура #шаблоны #шаблоны #objective-c #Swift #MVC #mvvm #mvp #viper #разработка iOS #проектирование и рефакторинг #objective-c #Swift
-
Молния Для Выездного Обслуживания
19 Oct, 24 -
Искусство Каждый День
19 Oct, 24 -
Какую Архитектуру Im Вы Считаете Приемлемой?
19 Oct, 24 -
Механические Компьютеры Возвращаются
19 Oct, 24