Как стать автором
Обновить

Комментарии 29

Это божественно. Спасибо большое за ваш труд.
Спасибо за отзыв, рад что не зря старался.
Хоть и не являюсь поклонником RxВсегоИВся, но статья написана очень интересно. Читал с удовольствием. Спасибо.
Я пока сам определяюсь буду ли использовать и в каком виде Rx в своих приложениях. Надо хотя бы на парочке pet проектов опробовать, потом уже делать выводы.
Хорошо, как смогу сформулировать для себя — опишу свой долгий путь к «идеальной архитектуре» )
Хотелось бы почитать про RxSwift + MVVM. Как правильно биндить viewModel, как передавать данные на другой viewController и т.п. =)
Вот как раз над этим сейчас и работаю. Перебираю возможные реализации. Т.к. к сожалению даже в Rx примерах архитектура на мой взгляд говоря не ахти, хоть там и заявлен MVVM, но создавать из ViewController — ViewModel, причем передавая в инициализацию сервисы — так себе идея. Ищу баланс между чистотой кода, расширяемостью и количеством классов на отдельный юнит.
Здорово. С нетерпением жду статью =)
На недели планировал написание о архитектуре с реактивом.
Используем очень плотно реактив (правда RAC2/RAC4 вместо RxSwift) и уже не один проект успешно живет и развивается. Пропитываем сквозь всю модульную архитектуру и нам это очень хорошо помогает разделять и лучше читать.
Ну и соотвественно за счет разделения отличная тестируемость достигается
С удовольствием почитаю. Статья основанная на успешном опыте будет крайне кстати.
Если вкратце, — MVVM?
Я вот сейчас как раз думаю как по уму это все применить. MVVM, + вынес все таки роутер из ViewController'а. Не очень нравится, что из VM в какой то мере получается god объект, он хоть и делегирует все сервисам, но все таки это уже не совсем чистый VM. Думаю как грамотно распилить VM и стоит ли это делать.
1) MVVM не чистый у нас в любом случае (что вызывает зависть у команды winphone :D) = разделение контроллера на presenter + viewmodel
2) применяем сюда SOA
3) роутер этот, вот на этот момент я написал мелкую штуку в 50 строк (долго думал нужно ли писать статью, но решился в сб и сейчас текст статьи на проверке/правке перед релизом, увы я не мастер писать связно там крайне мелкая статья в 1.5 экрана), но она позволяет убрать навигацию в отдельный объект полностью. А использование DI повышает переиспользование и тестирование еще выше

ах да, сториборды, сейчас на моем проекте 6 сторибордов ко всему этому
Ну значит я в верном направлении ковыляю.
Жду статью )
Как работают DelegateProxy? Они создаются и подписываются на все объекты подряд сразу после создания этих объектов или только когда это действительно необходимо?

Хочется оценить количество граблей в плане совместимости с другими библиотеками.
Стоит посмотреть на реализацию в _RXDelegateProxy.m. Если вкратце, — при создании прокси извлекаются все доступные методы и выставляются в качестве доступных. Так же в _RXObjcRuntime.m производится swizzle методов отвечающих за диспетчеризацию.
Таким образом при возникновении события — отрабатывает перехватчик из _RXDelegateProxy

-(void)forwardInvocation:(NSInvocation *)anInvocation {
    if (RX_is_method_signature_void(anInvocation.methodSignature)) {
        NSArray *arguments = RX_extract_arguments(anInvocation);
        [self interceptedSelector:anInvocation.selector withArguments:arguments];
    }
    
    if (self._forwardToDelegate && [self._forwardToDelegate respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self._forwardToDelegate];
    }
}


Если сигнатура метода есть в нашем списке — вызывается interceptedSelector уже для DelegateProxy

public override func interceptedSelector(selector: Selector, withArguments arguments: [AnyObject]!) {
    subjectsForSelector[selector]?.on(.Next(arguments))
}


subjectsForSelector же заполняется нужными сигнатурами, только когда мы делаем observe для delegate прокси.

public func observe(selector: Selector) -> Observable<[AnyObject]> {
        if hasWiredImplementationForSelector(selector) {
            print("Delegate proxy is already implementing `\(selector)`, a more performant way of registering might exist.")
        }

        if !self.respondsToSelector(selector) {
            rxFatalError("This class doesn't respond to selector \(selector)")
        }
        
        let subject = subjectsForSelector[selector]
        
        if let subject = subject {
            return subject
        }
        else {
            let subject = PublishSubject<[AnyObject]>()
            subjectsForSelector[selector] = subject
            return subject
        }
    }


Таким образом подписываемся мы вроде на все события, но DelegateProxy обрабатывает лишь те на которые есть подписка.

Далее вне зависимости от того отработал код в DelegateProxy или нет — если в _RXDelegateProxy есть forwardToDelegate — событие отправляется уже в оригинальный делегат
В каком репозитории располагается файл _RXDelegateProxy.m? Похоже, это что-то основанное на ReactiveCocoa, но я только знаю где лежит legacy-версия
Покопался в исходниках — создание DelegateProxy происходит лениво. НО он переустанавливается при каждой манипуляции с RX.

В общем, багрепорт: если к проекту, реализующая свой собственный DelegateProxy (пример из мира Swift'а не приведу, из ObjC — BlockKit) — делегаты имеют серьёзный шанс зациклиться друг на друга, потеряв делегат оригинальный и уйдя в бесконечные перевызовы друг друга.

Если коротко — DelegateProxy считает, что он владеет объектом эксклюзивно, хотя это может быть не так.
*если к проекту подключена библиотека, реализующая свой собственный DelegateProxy
Пардон. Не перечитал коммент.
Ну то, что прокси переустанавливается при несоответствии при каждой Rx манипуляции было в принципе освещено в статье в комментариях к содержимому функции proxyForObject
По поводу того баг ли это или нет я затрудняюсь сказать. Точнее понятно, что это потенциальная проблема. Но вот как решить. Можно дать возможность настраивать ядро Rx, чтобы какие то классы и их производные не перезаписывались проксей (в надежде что они уже выставили нашу прокси как свой forwardToDelegate). Не самый лучший подход. Можно кешировать rx_delegate, но тогда нужно следить за временем жизни прокси самому, ну или если после этого сверху что то было навернуто посредством BlocksKit, то наша прокси будет об этом не знать и все будет пролетать мимо BlocksKit. В общем простого решения нет, причем это не проблема Rx, а в принципе библиотек которые вмешиваются в работу runtime'а
Ведь в таком случае баг можно постить и на пресловутый BlocksKit.
И туда и туда, баг, на самом деле, отправлять надо. Просто сущность проблемы такова, что её можно обойти, если в программе используется не более одного sdk с этим багом.

Делегат, кстати, уже сейчас кешируется — записывается как associated object. Думаю, проблему решит установка делегата в случае, если у нас он не кеширован.

По поводу пролетать мимо — нет:
2 возможных случая очерёдности установки делегатов (после правки):
— BlockKit -> Rx: Сначала устанавливается делегат блоккита, захватывая оригинальный и подставляя себя. Потом — устанавливается делегат Rx, захватывая делегат блоккита. Вызов, упс, баг в блокките, зацикливание
— Rx -> BlockKit: Сначала устанавливается делегат Rx, захватывая оригинальный и подставляя себя. Потом — устанавливается делегат блоккита, захватывая делегат Rx. Вызов: Rx -> BlockKit -> original, всё отработало штатно.

Естественно, правку надо вносить в обе библиотеки, чтобы всё работало нормально.
P.S.: Все рассуждения построены с игнорированием того факта, что библиотека BlockKit написана на Obj-C и врядли они когда-нибудь встретятся вместе. Впрочем, неявные зависимости и всё такое… В общем, всё может быть.

P.S.: Но я упускаю возможность повторной установки делегата программистом. В общем — надо ещё что-то делать с setDelegate.
Для конкретных двух библиотек решение найти не проблема, вопрос в том как решить в общем виде. Ведь таких библиотек может быть и 20, в каждую вносить знание о каждой — никто не станет даже браться.
Приводить к единому стандарту? Опять таки вряд ли кто займется.
Хм. Безумная идея: добавить в каждый делегат метод вида __imIsAnRxDelerageProxy. Если текущий делегат отвечает на этот селектор — не подменять.
Это бы означало, что все обязаны знать про Rx, я думаю авторы других библиотек не особо обрадуются такому повороту.
Скорее если уж вводить общий API — определить, что если мы подменяем delegate то у прокси обязательно должен быть метод oldDelegate() -> NSObject
И каждая библиотека пусть при подмене делегата смотрит — есть ли такой метод, если есть — рекурсивно получает делегат, и если в итоге понимает что она уже есть где то глубже — ничего не подменять.
Но тут две проблемы.
1) это же надо ввести как стандарт, заставить авторов это реализовать
2) для какой то библиотеки может быть не критично насколько глубоко она стоит в иерархии oldDelegate, а для какой то обязательно надо быть первой. И рано или поздно найдутся библиотеки для которых кровь из носу надо быть первыми. И мы получим ту же проблему.

В общем каждый случай нужно рассматривать отдельно.
Общий API — это мёртвый номер. Найдутся и те, кто про негу просто не будет знать, и те, кто найдут в нём фатальный недостаток, и те, кто просто забьёт.

Имхо, нужно достаточно хорошо мимикрировить под старый делегат, чтобы другие библиотеки думали, что они по прежнему общаются со своим собственным объектом. NSProxy и всё такое. Но тут основная проблема — == по адресу.

Интересно, можно ли малой кровью передвинуть объект в памяти и поставить на его место свой прокси? :)
Ну я поэтому и сказал, что решать только в частном порядке.

Учитывая что идет сравнение по адресу памяти — крайне сомневаюсь, что получится что то сделать. Если бы была такая возможность, — просто представьте какие возможности это открыло бы перед злоумышленниками.
Я более чем уверен, что в своём приложении такое провернуть можно, главное убедить компилятор дать это сделать. В конце концов у нас есть указатель на интересующий нас адрес и мы можем в него писать.

Но внутренние структуры всё равно придётся перенести по новому адресу. Если я ничего не путаю, они таки константного размера. Другой вопрос — как к этому отнесётся arc. Да и не стал бы я такое в продакшн выкладывать, а тем более — в популярную библиотеку

Как альтернативный вариант — можно засвизлить методы существующего делегата. В общем, если нужны будут безумные идеи — обращайтесь. Их есть у меня :)
Ну что тут скажешь, всегда можно найти «Верёвку достаточной длины, чтобы выстрелить себе в ногу» (с)
Сложность возникает, когда переменная именована observable, а должна быть observer (а это понятно только из контекста, что неверно).

Всегда старались разработчики именовать переменные непохожим образом (target-action, event-source, source-tagret). Так нет, надо было разработчикам Rx одинаково назвать сущности, чтобы потом путать и путаться. Еще и лишне напрягаться приходится, чтобы понять и разобраться, что же автор имел в виду.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории