Pull to refresh

Comments 6

В статье про основы «реактива», мне кажется стоит отдельно упомянуть, что реактив имеет несколько важных контрактов, которые нельзя нарушать:
  • поток заканчивается или с onComplete или с onError, но никак не с двумя одновременно
  • onComplete или onError действительно прекращают поток и после их вызова нельзя эмитить другие элементы

Для кого-то это может показаться очевидным, однако, если создавать потоки самостоятельно или использовать Subject-ы, этот контракт легко можно сломать. А от сломанного контракта станут неправильно работать многие стандартные операторы типа concat, toList, last и много других.
Спасибо за статью.
Я пока подзаморозил написание своей по применению RxSwift, хоть она уже и была написана на половину, т.к. столкнулся с вопросами на которые у меня у самого нет «красивых» ответов.
Т.к. RxSwift, RAC — это реализации идеи функционального программирования — разумно использовать их мощь на полную.
Я же в итоге пришел к тому, что Rx отлично вписывается в биндинг с UI, но постоянно попадаются подводные камни.

Главная загвоздка в том, что идея реактивного программирования является — отсутствие состояния (в идеале). Отсюда идут настоятельные рекомендации подписываться на Observable только из UI (соответственно disposeBag должен быть только в ViewController'ах), т.к. подписки автоматически освободятся когда сменится экран. Т.е. та же ViewModel не должна иметь состояния.

Классическая схема MVVM если один контроллер создает другой
ParentViewController -> ParentViewModel -> ParentModel
ChildViewController -> ChildViewModel -> ChildModel

Причем ViewController знает о ViewModel, но не создает. А ParentViewModel — создает СhildViewModel
На все подписки по хорошему мы должны подписываться только в View
Ну и использовать переменные крайне не рекомендуется, только Observable только хардкоръ.

Но рассмотрим простую ситуацию.
Есть ViewController содержащий элементы логина + TableViewController (посредством контейнера).
Опять таки, по канонам ViewModel для TableViewController должен создавать ViewControllerModel.

ViewControllerViewModel когфигурируется из UI

class ViewControllerViewModel {
	func configure(login login: Observable<String>, password: Observable<String>, moduleId: Observable<String>, buttonParse: Observable<Void>) {
		...
		// внутри после всех проверок логина на пустоту и прочего в итоге мы получаем что то вроде этого

		let items: Observable<[DownloadTaskModel]> = parseState
		            .flatMapLatest{ (state) -> Observable<[DownloadTaskModel]> in
		                if case .Success(let data) = state {
		                    return Observable.just(data)
		                } else {
		                    return Observable.just([])
		                }
		            }.startWith([])
		        
		// и тут нам надо передать данные для TableViewModel, и мы не можем подписываться отсюда ни на что, т.к. мы внутри viewModel. Можно вытащить observable наружу, чтобы ViewController этой ViewModel "впустую" на него подписался, но это грабли.
		        tableViewModel.configure(items)

	}
}


У TableViewController же получается отрабатывает viewDidLoad раньше чем у его родителя ViewController, поэтому к моменту загрузки таблицы — у нас еще нет для нее данных, и приходится опять таки городить грабли

class TableViewControllerViewModel: NSObject, TableViewControllerViewModelProtocol {
    var session: NSURLSessionProtocol
    
    var disposeBag = DisposeBag()
    
    var dataSourceItems: Driver<[NumberSection]>! {
        didSet {
            completeConfigure.onNext()
        }
    }
    
    let completeConfigure: PublishSubject<Void> = PublishSubject()
    
    init(session: NSURLSessionProtocol = SessionFactory.sharedInstance.backgroundSession()) {
        self.session = session
        super.init()
    }
    
    func configure(items: Observable<[DownloadTaskModel]>) {
        
        dataSourceItems = items.asObservable().doOnNext{ [unowned self] _ in
            self.disposeBag = DisposeBag()
        }
            .map{[unowned self] models -> [NumberSection] in
                let protocols = models.map{ model -> DownloadableTaskViewModelProtocolThing in
                    let downloadDelegate = Downloader(session: self.session)
                    downloadDelegate.update.subscribe().addDisposableTo(self.disposeBag)
                    let cellViewModel = DownloadTaskViewModel(model: model, downloadDelegate: downloadDelegate)
                    downloadDelegate.updateStatusIfAlreadyDownloaded()
                    return DownloadableTaskViewModelProtocolThing(thing: cellViewModel)
                }

            }.asDriver(onErrorJustReturn: [])
    }
}


В TableViewController приходится подписываться на completeConfigure, и только после получения сигнала — конфигурировать dataSource для таблицы.

Есть альтернатива — делать dataSourceItems как переменную, изначально пустую и в configure — присваивать значение этой переменной.
Но я перечитал все issue где разработчик RxSwift отвечал на вопросы, и он черным по белому говорил — если вам приходится использовать переменную, либо вы что то делаете неправильно, либо у вас нет другого выхода. ViewModel не должна иметь состояния.
И несколько устаешь каждый раз думать первый это случай или второй.

Так же мне очень не нравится, что внутри ViewModel есть DisposeBag. А нужен он потому что нам надо подписываться на Observable update контроллера по закачке. Пробрасывать в TableViewController не получится, т.к. при каждом переконфигурировании данных для таблицы надо старую подписку сбрасывать.

Второй пример.
Есть DownloadController, его задача реагировать на нажатие кнопки у закачки соответственно,

В классическом варианте мы имеем что то вроде такого

class DownloadController {
...
func download() {
    switch model.status {
    case .Progress, .Wait:
        model.status = networkTaskManager.pause()
    case .Stopped, .Error:
        model.status = .Wait
        let task = session.downloadTaskWithURL(assetUrl())
        networkTaskManager.startPrepareTask(task)
    case .Paused:
        if let data = networkTaskManager.resumeData {
            model.status = .Wait
            let task = session.downloadTaskWithResumeData(data)
            networkTaskManager.startTask(task)
        } else {
            let task = session.downloadTaskWithURL(assetUrl())
            networkTaskManager.startTask(task)
        }
}
}


Model имеет биндинги, поэтому мы спокойно обновляем ее состояние, которое пробрасывается в ViewModel.

Когда я попробовал избавиться от состояния, то столкнулся с тем что код лишь усложнился. Как видно изменение статуса — не только результат операции — но и может меняться перед и во время операции, поэтому просто представить это как dataFlow уже не получается. Приходится делать постоянные сайд эффекты .doOnNext{ model.status =… } что снова таки по словам разработчика RxSwift — ugly hack, применение которого означает что вы что то делаете не так.
И в итоге получилось лишь усложнение.

Были еще моменты и не мало. Написал то что сразу в памяти всплыло.
Я скорее всего просто не умею готовить правильно FRP, признаю. Наверное это и есть steep learning curve. Но когда я понял, что я трачу 80% времени на то чтобы сделать «правильно по канонам FRP» вместо того чтобы писать полезный код, я отступил назад.
В итоге вернулся на шаг назад, — сделал самописный Bind, для простых задач связать Model->ViewModel->ViewController его хватает. Я конечно потерял много плюшек от RxSwift. Но признаться меня еще как то внезапно напрягло то, что это все таки не пара классов, это фреймворк, который пока мало кто использует. Как и саму парадигму FRP.
Т.к. в данный момент я лишь планирую выйти на рынок как фрилансер наверное было бы глупо уходить с головой в FRP, сомневаюсь что это нужно на небольших проектах, а как обьяснить клиенту, зачем я буду подключать ему целый фреймворк я пока не знаю.
Так что я пока взял паузу в наших отношениях с RxSwift.
При этом я ценю идею immutable заложенную в основу FRP, и данные сейчас стараюсь хранить в структурах, а не классах. Это тоже накладывает ограничения, и доставляет определенные неудобства (особенно весело с CoreData). Но их намного проще обойти и польза от них наглядная, причем все это бесплатно, без фреймворков.
Я не говорю, что FRP — плохо, но надо потратить не мало времени, чтобы умело пользоваться его арсеналом, при этом научиться обходить его ограничения.
Я пока остановился на модифицированном MVVM: (Router — ViewController — ViewModel — Model + Service Layer) При этом Model — структуры, что позволяет контролировать изменение данных. Ну и как я говорил — самописный биндинг.
огромный комментарий :D

1) По мне абсолютно нормальная практика когда ViewModel имеет переменные (уже как минимум если используем биндинги), хотя бы на уровне PropertyType. Потому что это ViewModel и это его задача прямая взять на себя работу с данными для экрана. Либо мы говорим о разных понятиях под ViewModel.
Взаимодействие Controller — ViewModel будет работать как раз на сигналах, ViewModel будет иметь сигналы, в которые будет сообщать о ситуациях нужных для контроллера. Либо создавать и отдавать их обратно контроллеру на действия, в зависимости от ситуации.
Конечно, можно разнести все такие сигналы в PropertyType, но жизнь от этого проще не станет.

2) О создании, кто что создает… намного лучше взять DI фреймворк (Swinject к примеру)

3) В чем проблема от использования структур и кордаты? Это ведь совершенно разные вещи и между ними взаимодействие только на уровне DAO должно осуществляться. Более того, используя структуру мы можем забить на подход с использованием линз тк это копируемый тип и оставить поля в структурах как мутабельные.

В общем, если нам позволяется совмещать несколько подходов в одном для более быстрого и удобного достижения нужной цели, то это надо использовать. Наш подход состоит в следующем:

Controller — работа с UI + взаимодействие с ViewModel (подписки на сигналы и получение сигналов для действий)
—— ниже больше никто не работает с UI компонентами и работают только с данными.
ViewModel — подготовка данных для Controller + взаимодействие с Service
Service — инкапсуляция работы, не связанная с экранами на прямую. Прячем сюда всю черную работу попутно убирая возможный повтор действий.
—— Service взаимодействует с различными компонентами системы, в зависимости от ситуации.

p.s. реактив должен облегчать работу, а не заставить отказаться от парадигмы вашего языка. К примеру попытка сделать абсолютно все immutable.
p.p.s. даже в функциональных языках существуют монады.
Да уж, не маленький, накипело ))
1) под переменной подразумевается Variable из RxSwift, класс позволяющий напрямую засунуть в нее данные, не из потока Rx. Понятно что по сути все переменные Observable динамически меняют данные в зависимости от того что на входах к которым они подсоединены, но это вписывается в dataFlow. А тут как бы точка выхода в обычный мир, не реактивный.
2) Да я понимаю, что для всего есть фреймворки. Но изначально хотелось понаступать в pet проекте на все грабельки, чтобы собрать опыт и потом знать чего не хватает для полного счастья.
3) Проблема не столько из за использования структур, сколько от в принципе попытки делать обертку над NSManagedObject. Проблемы когда у NSManagedObject'ов есть связи, их приходится оставлять в виде objectid, грубо говоря не deep копирование. Иначе при создании одного объекта — он рекурсивно начнет создавать все до чего дотянется. В CoreData то используется Faulting.

В Service передаете те же data модели что и во viewModel, или вы конвертируете их в облегченные, чтобы не тащить все?
Между Model и ViewModel есть биндинги? Соответственно когда Service что то меняет — он отдает это в Rx виде или просто кусок данных? Или меняет model внутри себя, а модель благодаря биндингам уведомляет всех включая viewModel?
Если через биндинги, то на каком этапе фильтруете данные? Если Service что то хочет поменять в Model, но эти данные не валидны — он сам это определяет и в model всегда корректные данные, или один из подписчиков на model, какой нибудь валидатор это разруливает?

p.s. да я понимаю, что это гонка за единорогом, сделать все Правильно (с) Но по большому счет напрягает именно необходимость тащить фреймворк в зависимость. Работая в конторе — одно дело, там проекты побольше, планирование, наработанные практики. В одиночку надо пройти этот путь на паре своих проектов прежде чем предлагать клиенту.
Даже если все сделать на переменных (Variable, PropertyType), то в них всеравно придется биндить сигналы (не скажу в этом случае за rx, но в rac это создаст Disposable и его нужно будет положить в composite (bag)). И всеравно ViewModel будет иметь по этому bag, по этому не вижу ничего плохого если будет подписка на сигналы не только во ViewController.
Кстати, если все перевести в такие переменные, сложно сопоставлять будет действия когда придет новый человек. А вот если сервисы выдают сигналы и на них подписываются из Presentation (ViewModel к примеру и потом выдает результат по необходимости в Controller на обновления) то сложного ничего нет, потому что разделяются обязанности: сервис сконфигурировал, ViewModel подготовил данные и обновился Controller

Биндинга между моделью и вьюмоделью не делаем, сервис выдает структуры, по этому сохраняется иммутабельность данных.
Если есть к примеру необходимость при переходе на экран всегда показывать актуальные данные, значит есть смысл каждый раз при показе экрана эти данные просить у сервиса из «кэшируемых». Это накладывает свои ограничения, но явно показывает принцип работы этого экрана. К тому же пропадает расхождение между хранимыми и показываемыми данными (если разумеется их показывают «как есть»).
Опять же, если полученные данные из сервиса изменить, это будет только локальным изменением.
Ну у меня была задача обновлять процент скачанного в прогресс барах как основного контроллера так и в отдельном экране, поэтому сделал модель структурой, положил ее в в Variable, и в сервисе отдавал именно в обертке. При изменении сервисом процента в структуре — viewModel подписанные на изменение Variable — обновляет все свои переменные, которые по биндингам уходят во ViewController.
Это позволило оставить модель «тупой».
Из минусов подхода.
Если модель будет классом — придется после каждого изменения в сервисе делать updateModel (переприсваивать саму себя в переменную)
Если модель будет структурой — изменение любого свойства — автоматом обновит всю модель о чем узнает ViewModel. Но если надо поменять последовательно 10 свойств — каждое изменение будет обновлять всю модель. В качестве обходного пути — сначала достать структуру из переменной, обновить все в ней — и только потом присвоить обратно.

Кстати не заметно просадки по производительности при использовании RAC? И сколько добавляет в весе программы использование этого фреймворка?
Sign up to leave a comment.

Articles