Проектирование наблюдаемых объектов - PullRequest
1 голос
/ 07 ноября 2019

Предисловие: это вопрос разработки о реактивном программировании. Он предназначен быть независимым от языка, так что все это псевдо-кодей. Я подозреваю, что любой правильный ответ будет одинаково применим к Rx / ReactiveCocoa / Combine.

Я вижу 3 различных способа, которыми я могу проектировать объект для наблюдения. У каждого из них, безусловно, есть свои плюсы и минусы, но не совсем понятно, кем они являются.

  1. Ваш объект может иметь свойство didChange: Publisher<()>. Когда вы подписываетесь, вы получаете уведомления о том, когда ваш опубликованный объект изменяется, но вы фактически ничего не узнаете об изменениях или новом значении модельного объекта. Это достаточно просто исправить с помощью map:

    class Point: BindableObject {
        var x, y: Int
        var didChange: Publisher<()> { ... }
    }
    let object: Point = ...
    let streamOfPoints = object.didChange //  Publisher<()>
                                .map { _ in object }  // Publisher<Point>
    

    . Вы просто получаете значения объекта для себя, обращаясь к объекту напрямую по какой-либо другой ссылке, когда получаете уведомление о том, что онменяется. Если вам нужен доступ к потоку значений x или y, они также могут быть на расстоянии всего одного вызова map.

    Однако, похоже, у него есть некоторые проблемы.

    1. Это дополнительный шаг
    2. Требуется, чтобы у вас был доступ к исходному объекту, поэтому отсылки издателя недостаточно. Вы должны обойти пару (Point, Publisher<Point>), которая кажется громоздкой.
    3. У нее могут быть проблемы с корректностью. Например, любая задержка между временем возникновения события didChange и доступом к объекту, не возможно ли, что вы прочитаете более новое значение объекта, чем то, которое присутствовало, когда изменение было запущено?

      Этот подход мне менее всего нравится, но, что интересно, именно этот подход Apple использует в своей структуре Combine с протоколом BindableObject. Я подозреваю, что это может быть связано с каким-то приростом производительности, а не с тем, чтобы обходить объект в ситуациях, когда он не нужен. Так ли это?

  2. Наиболее очевидный подход - потоковая передача самого объекта. var didChange: Publisher<Point> { /* a publisher that emits self over time */ }. Похоже, это достигло того же уровня, что и в apporach # 1, при решении 3 перечисленных мною проблем. Я не вижу никакого значения, которое предлагает подход № 1 к этому.

  3. Вы можете сделать издателей для каждого из полей объекта:

    class Point: BindableObject {
        let x = PublishSubject<Int>
        let y = PublishSubject<Int>
    }
    

    Thisявляется более детальным, поэтому люди могут подчиняться только тем областям, которые им небезразличны. Я не знаю, как обстоят дела с подписками с большим весом, но может быть выигрыш в производительности, если подписаться более конкретно только на те вещи, которые вас интересуют. Это немного надуманный пример, потому что трудно представить себе случай, когда желательно знать только значения x или y точки. Но этот принцип все еще в целом применим.

    Доступ к xs и ys также возможен с использованием одного из первых двух подходов, путем сопоставления потока с xs и дедупликации (.map { $0.x }.distinct). Но это вызывает гораздо больше замыканий сопоставления, чем прямая подписка, подобная этой, что может повлиять на производительность.

    Этот подход также можно объединить с подходом 1 или 2, чтобы добавить свойство var didChange типа Publisher<()> или Publisher<Point>, когда вам нужно наблюдать за всем точечным объектом.

    Раньше это вызывало много раздувания API. В Rx, либо:

    1. Либо у вас может быть x: Value<Int>, и вы можете использовать x.value повсеместно для доступа к текущему значению
    2. Или у вас есть var xObservable: Value<Int> и var x: Int { xObservable.value }, но это добавляет много раздувания API.

      К счастью, эти две проблемы решаются с помощью оболочек свойств, в основном реализуя последний дизайн, но без необходимости явного добавления всех вычисляемых свойств(они созданы для вас).

Не могли бы вы дать несколько советов по поводу того, какой шаблон использовать? Я подозреваю, что это одна из тех вещей, которые "очевидны с опытом", но я просто еще не там, когда дело доходит до реактивного программирования. Спасибо!

1 Ответ

1 голос
/ 08 ноября 2019

В этом вопросе есть что взять, но я бы сказал, что нужно выбрать вариант 2.

Я не очень разбираюсь в разработке реактивных сред, но, глядя на ваши варианты, мыможет в значительной степени исключить первый вариант, поскольку он слишком ограничен в использовании, что оставляет варианты 2 и 3.

Вариант 3 допускает большую степень детализации, как вы упоминаете, но, на мой взгляд, такая степень детализации вызывает многобольше раздувания для обычных случаев использования - например, меня интересует вся измененная модель, а с .map и .filter я могу легко уменьшить поток в опции 2 до того, что было бы гранулированным вариантом, когда это было необходимо легко,Мне кажется, что чаще всего дело в том, чтобы смотреть на всю наблюдаемую модель, а не на ее часть.

В настоящее время также кажется, что вариант 2 просто преобладает в дизайне Rxи подобные структуры, и я бы сказал, что это просто опытный способ сделать это.

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

...