Pull to refresh

Comments 18

Я сделал несколько другую вещь, когда решал похожую задачу.

1. Определил класс, в котором хранились подписанты на событие. Массив пар «объект-селектор».
2. Определил у этого класса метод invoke в трех вариантах (без агрументов, с sender и с args)
3. В этих методах написал код, определяющий количество фактических параметров у подписанта и соответствуюший вызов
4. Сделал пару макросов типа #define EVENT(eventName) которые разворачивались в add##eventName##handler:args: и remove##eventName##handler:args
Получилось проще и безо всяких шаблонов и прочего С++.
Я тоже с этого начал:) Я вообще раньше Flash-ом занимался и там это usual way для Observer-а.

Возникающие проблемы:
1) нет проверки аргументов, на больших проектах выливается в критические места кода и копание в нём же
2) для таких вещей я предпочитаю блоки, а не селекторы, по причине того, что селекторы оставляют ссылку на объект, относительно которого их надо вызывать, это чаще приводит к утечкам памяти
3) у меня логика диспатчинга хранится внутри сигнала, он обособлен и это лучше, чем передача по интерфейсу, правда это имхо
4) вытекает из 3: необходимость наследоваться (либо тянуть категорию) логики «в котором хранились подписанты и метод invoke»

Правда, если лично Вам так удобней — то это тоже имеет место быть, но я решил всё же пойти дальше, т.к. статическая типизация аргументов invoke-а, подказка типов параметров для addObserver-а и тому подобное — сильно подкупают:)
Селектор не оставляет никакой ссылки на объект, вы о чем?

То, что объект, который подписан на событие должен жить дольше, чем объект, который эти события выдает (или вовремя отписываться) или что?

У меня логика диспетчиризации описана в этом объекте. Его надо аггрегировать и подключать через макрос.
необходимость передавать не только селектор, но и объект, относительно которого применяется этот селектор, я уже написал, прочтите ещё раз:)

На него то и останется ссылка.

С теми же блоками можно сделать:
_block MyViewController *weakSelf = self;

auto observerBlock = ^(id target, NSString *stringParam, BOOL boolParam)
    {
        [weakSelf doSomethingWithString:stringParam];
    };
необходимость передавать не только селектор, но и объект, относительно которого применяется этот селектор, я уже написал, прочтите ещё раз:)

На него то и останется ссылка.

Можно использовать __weak ссылку, и никаких проблем.

Лично мне в селекторах не нравится:
1) Компилятор не проверяет корректность написания имени селектора
2) При наличии аргументов, для определения их типа надо лезть в документацию

В блоках, к слову, тоже есть один, но серьезный недостаток: из-за того, что они retain'ят используемые объекты, есть риски искусственного продлевания срока жизни объектов или даже возникновения retain cycle'ов.

А можно раскрыть подробнее вот эти моменты:
так же идентификация событий идёт по строковому идентификатору, что не есть хорошо. Ну и производительность такого решения оставляет желать лучшего, как и API.
и
но API сводит все его прелести на нет

Хотя бы ссылками на обсуждение где-либо, какими-либо benchmark-ами, которые показывают плохую производительность и т.п. Другими словами, ваша статья начинается с удтверждения «Notification Center и KVO — плохо» и это подается как всем известный и неоспоримый факт. Почему строковый идентификатор это плохо? Насколько «медленно» работает Notification Center? Почему API у NC и KVO «плохой» и «сводит все прелести на нет»?

Без подробных обоснований создается впечатление что вы пытаетесь потянуть за собой то, что считается «usual way» для Observer-а в Flash только потому что вы к этому привыкли.

Вы не совсем верно меня поняли с «usual way», ибо я как раз таки не пошёл по нему и реализовал решение конкретно под данную платформу. Но на Ваш вопрос я всё же отвечу, ибо он имеет место быть, но не в контексте «их не надо использовать, они плохие» а «какие у них есть слабые места». Позвольте ссылками:
KVO:
1) www.mikeash.com/pyblog/key-value-observing-done-right.html
2) www.mikeash.com/pyblog/friday-qa-2012-03-02-key-value-observing-done-right-take-2.html
3) от себя добавлю, что KVO — это работа с строковыми keyPath-ами, что так же повышает вероятность ошибки

NotificationCenter:
1) robnapier.net/blog/thoughts-nsnotifications-42
2) от себя хочу лишь сказать, что, во-первых, меня смущают строковые ID событий (не все программисты знают, что для них надо заводить константы, чтобы не было потом проблем), во-вторых — отсутствие типизированного способа передать параметры в слушатели события (сейчашний вариант лишь передавая object в postNotification, и если нужна жесткая типизация — то приходится создавать по классу на каждый такой тип события). А так же общая проблема с KVO — это crash системы на стадии dealloc объекта, если программист не отписался от событий KVO или NC.

Во флеше я использовал EventDIspatcher, который очень близок к NC по API подписки\отписки, и подводные камни обоих решений выявляются довольно-таки быстро
Слабые места связанные со строковыми идентификаторами есть. Все верно по поводу observeValueForKeyPath:... и теми случаями, когда суперкласс зарегистрировался на тот же key path. Но, одно большое «но», даже много их…

«Слабость» этих мест определяется слабостью программиста. И это беда уже не Objective-C или конкретных API. На том же C неграмотный прогер наваяет такого, что просто ужас, одни только malloc или memcpy чего стоят, на embedded платформах это вообще опаснейшее «оружие». Приходилось сталкиваться бесчисленное множество раз, когда люди не могут по-человечески выделить и освободить кусок памяти, а то и копируют туда больше чем положено.

Судя по вашим комментам, приходится иметь дело с аутсорсом и недостаточно грамотными разработчиками. Это беда, но новое и более навороченное решение с использованием C++ и шаблонов, имхо, только увеличит вероятность ошибок. Если эти ребята порют бока с обычным KVO и NC, так ли легко им будет разобраться с вашим кодом?

И раз уж речь идет про аутсорс. Использование KVO и NC — гораздо более безопасный подход, хотя бы потому что им пользуются все, он хорошо документирован и т.д. Поставьте себя на место аутсорсера, которому достанется ваш код, по непонятным ему причинам весь сплошь из .mm файлов и с какой-то кастомной имплементацией observing-а.

Насчет низкой производительности KVO и NC, кстати, я по ссылкам ничего не нашел.

Как вывод, это хорошо, что есь более удобный и (возможно) более понятный способ сделать KVO и уведомления. Только по этой статье и по коментариям к ней я насчитал уже как минимум 3 реализации. Но, лично я не хотел бы видеть ни одной из них в следующем проекте, который мне придется поддерживать или принимать :) Пусть уж лучше будут KVO и NC со всеми их хорошо известными недостатками, чем каждый раз что-то новое и кто знает насколько хорошее.
Чем мне нравится iOS — в ней много вещей не делаются из расчета «это беда не наша, а слабого юзера\программиста», и я считаю что это львиная доля успеха данной платформы в целом. Поэтому поиск удобных, универсальных и лёгких для вхождения решений, а главное безопасных — всегда будет актуальным, как я считаю.

Вы правы, по работе часто приходилось работать в аутсорсе, и ещё чаще — руководить программистами, следить за их кодом, проводить код ревью, и у меня появилось железное правило, что нельзя им доверять. Нельзя просто доверить реализацию чего-то спорного, чтобы потом сказать «программист был недостаточно сильным», менеджеры этого не оценят, поэтому мне важно быть уверенным в технологиях, которые мы используем. И строковая идентификация, к примеру — крайне частая причина ошибок, которые ещё и не легко дебажить, именно поэтому я зацепился за эту тему и начал искать решение проблемы, что вылилось в итоге в отхождение от темы в сторону Observer-а. А паттерны они на то и паттерны, что вы прийдёте в незнакомый проект, увидите, что там есть некие сигналы, у них вызывают «addObserver\removeObserver» и «notify», и если вы «сильный программист», то вы сразу же узнаете этот паттерн и будете знать, как с ним работать:)

P.S. Не понимаю, кстати, боязни работать с Objective-C++, ведь кроме как смены расширения файла и редкого вкрапления С++ кода в основной код это больше ничего не меняет, но даёт кучу преимуществ мира С++, таких как шаблоны, например:)
Соглашусь с i4niac. Мне тоже показалось после прочтения, что использовать NC и KVO проще и понятней, нежели вплетать в код плюсовые шаблоны. Разве API NC выглядят хуже, чем плюсовые шаблоны в Objective-C++ коде?

    // some place
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSomethingChanged:) name:kMyClassSomethingChanged object:someObject];

    // other place
    [[NSNotificationCenter defaultCenter] postNotification:kMyClassSomethingChanged object:self userInfo:someUserInfoDictionary];

Код очень хорошо читается, каждый параметр весьма осмысленный. И с возможностью передавать произвольное количество параметров через userInfo.

А что до «строковых идентификаторов», так ведь и любой вызов метода в Objective-C идёт через строковый идентификатор селектора. Обычно тип SEL представляет из себя такую структуру:

typedef const struct objc_selector
{
   void *sel_id;
   const char *sel_types;
} *SEL;

Конечно, это определение компиляторозависимо, в некоторых случаях первое поле определяется так же как char *, но сути это не меняет: селектор это строка.
Буквально только ответил i4niac :)

Поймите, я не боюсь строк, а в случае с селекторами у нас ещё и warning об отсутствии такого селектора на объекте. Я боюсь отсутствия какой-либо проверки и хотя бы warning-а когда программист пишет
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSomethingChanged:) name:@"somethingHappened" object:someObject];


и API NC это легко позволяет.

а про
Разве API NC выглядят хуже, чем плюсовые шаблоны в Objective-C++ коде?

Да, и могу сказать почему:
1) Какие ключи будут у someUserInfoDictionary?
2) Каких типов?
3) Что будет, если программист А создал константу @«MyGlobalSomethingHappened», а потом программист B (который сидит через океан от программиста А) по случайному стечению обстоятельств тоже случайно завёл такую костанту и постит свои события, даже и не зная, что они конфликтуют с программистом А?
Я боюсь отсутствия какой-либо проверки и хотя бы warning-а когда программист пишет...

Ну, это уже претензия к слабой типизации. Возьмите любой язык без строгой тирпизации и он Вас этим не устроит. А кого-то другого — устроит именно этим.

Холивары на тему «строгая и не очень типизация» — популярная штука.

Что будет, если программист А создал константу… а потом программист B… тоже случайно завёл такую костанту

Если они создадут эти константы в одном проекте, то будет ошибка линковки. Если в разных — то всем пофигу, разве нет? )

Вы беспокоитесь, что некий программист может легко накосячить с NC и KVO. Но если уж он тут накосячит, то это не самый сильный программист, ясно дело. А значит, и с шаблонами у него проблемы скорее всего будут.
Ну, это уже претензия к слабой типизации.

Вы вообще мои сообщения читаете, или только по диагонали пробегаете?:) Вы сейчас очень эпично вырвали фразу из контекста идентификации события по строке, а не по объекту или указателю! В плюсах, в Java или в любом другом жёстко типизированном языке можно написать библиотеку, которая будет принимать строку и по ней диспатчить события, сути не поменяет. Какие к черту холивары про типизацию?

Если они создадут эти константы в одном проекте, то будет ошибка линковки. Если в разных — то всем пофигу, разве нет? )

#define kMyGlobalSomethingHappened = @"MyGlobalSomethingHappened"
#define MyGlobalSomethingHappened = @"MyGlobalSomethingHappened"
#define MY_GLOBAL_SOMETHING_HAPPENED = @"MyGlobalSomethingHappened"

Вот вам пример конфликта из-за того, что один из программистов не учел правило написания констант NC, которые везде советуют писать идентично содержанию константы. Это в одном проекте. А что, если Вы подключили либу, в которой используеца NC?

То, что предлагаете Вы — не работает в крупных проектах, где нельзя просто сказать «этот баг мы исправляли 2 недели, а не 2 дня, т.к. у нас в команде из 30ти человек работает не самый сильный программист, который случайно имя константы (sic!) чуть чуть не так написал»
Вы вообще мои сообщения читаете

Читаю, да =)

Я говорил про ворнинги компилятора и неизвестность внутри userInfo.

Вот вам пример конфликта

Пример синтаксически не верен, но суть ясна. Кроме того, такие штуки обычно объявляются через extern NSString *.

один из программистов не учел правило написания констант NC

Так для того и есть coding style guidelines, от Apple даже, кстати. И внутри команды надо так же их придерживаться, будь эта команда из 2-х или 50 человек. Если в большой команде есть слабые звенья, их код подлежит строгому review, иначе будет плохо.

Так же полезно регулярно проходиться по коду статическим анализатором.

Я просто пытаюсь высказать мысль, что тут не инструмент виноват, а программист. NC — удобная и универсальная штука, но, как и всё остальное, не лишена недостатков. И если грамотно её использовать, проблем не будет.
А я и не говорил, что его не надо использовать, где Вы такое прочитали?:)

Не существует универсального решения, всегда для каждой задачи решение подбирается индивидуально, и Observer — это одно из решений задачи «локальных» оповещений, где требуется передавать типизированные параметры, иметь возможность легко подписываться и отписываться, а так же строго проверять соответствие подписчиков диспетчерам, вот и всё)

И немного оффтоп:
Вам приходилось работать в больших командах программистов? В outsource? Ничего личного, но просто мне кажется, что у Вас не большой опыт работы, когда нельзя полагаться только на свои навыки и знания, и поэтому Вы так максималистки воспринимаете «Если программист не пишет по Coding Style Guidelines — то он не должен работать в команде»:) гайдлайны — это лишь направляющие правила, но есть фирмы, где внутренние правила могут отличаться от них, а новый программист об этом не будет знать, либо по непривычке забудет.
Не существует универсального решения

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

Вам приходилось работать в больших командах программистов?

Да

В outsource?

Нет

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

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

Оффтопик номер 2.
Кстати, на данный момент мы переписываем практически с нуля весь код, пришедший от аутсорсеров. Ибо в нём не были соблюдены никакий гайдлайны, полностью отсутствовала структура и логика проекта, что делало сопровождение и развитие кода очень дорогостоящими вещами. Переписать в итоге куда легче.
Любопытно.
Мы у себя используем похожую библиотеку. Мотивация была примерна та же, что у Вас. Помимо сказанного выше, в NC напрягала необходимость отписываться в dealloc'е, особенно при использовании блоков, — тогда надо где-то хранить observer для дальнейшего отписывания.
В результате написали класс RSEvent, в котором хранятся блоки-обработчики.
// декларация события в .h
@property (nonatomic,readonly) RSEvent *viewDidLoadEvent;

// инициализация .m
_viewDidLoadEvent = [RSEvent new];

// запускаем событие
[self.viewDidLoadEvent fire];

// подписываемся
[self.viewController.viewDidLoadEvent addHandler:^{
    // ...
} owner:self];

Основная цель была — обеспечить максимально ясный интерфейс и простоту подписки.
Поэтому, чтобы не делать отписку в dealloc'е, предусмотрели параметр owner: при уничтожении owner handler автоматически удалится из события.

Возможно у нас такая специфика приложений, но большинство событий не требуют аргументов. Там же, где требуются, добавляем в интерфейсе методы, а в реализации используем тот же event:
//.h
-(NSString *)addViewDidLoadHandler:(void(^)(UIView *view))handler owner:(id)owner;
-(void)removeHandler:(NSString *)uid;

//.m
-(NSString *)addViewDidLoadHandler:(void(^)(UIView *view))handler owner:(id)owner{
    __typeof(self) __weak weakSelf = self;
    return [self.viewDidLoadEvent addHandler:^{
        __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) return;
        if (handler) handler(strongSelf.view);
    } owner:owner];
}
-(void)removeHandler:(NSString *)uid{
    [self.viewDidLoadEvent removeHandler:uid];
}

// подписка
[self.viewController addViewDidLoadHandler:^(UIView *view) {
    // ...
} owner:self];

Реализация подписки с аргументами конечно тяжеловата.
Но зато: интерфейс очень простой, автоматическая отписка при dealloc'е, автокомплит, корректность использования интерфейса проверяется компилятором.

Ваш подход с применением C++ мне понравилася, но к сожалению он не удовлетворяет нашей главной цели простоты интерфейса подписки: обязательный переход на Objective-C++ сюда не вписывается.
По Вашей реализации есть пара вопросов, напишу ниже.
Вопросы.
1. В блок-наблюдатель первым параметром передаётся TLSignal<UIView *> *signal. Зачем он вообще нужен? На мой взгляд, это только усложняет интерфейс. А выше, кстати, сказано другое: что первым параметром передаётся «держатель» сигнала.
2. Для отписки нужно передать сам handler. Это неудобно, а иногда и невозможно. Возможно, лучше использовать строковые токены?
3. Автоматической отписки я так понял нет? Т.е. если существует например долгоживущий сервис с сигналом, на который временные объекты навешивают блоки, то после удаления этих временных объектов эти блоки останутся висеть в сигнале «мусором»?
Если так, то на мой взгляд, это существенный недостаток.
Sign up to leave a comment.

Articles