Is there a more 'Combine' way to share state within an object hierarchy?

Viewed 68

I'm trying to incorporate Combine in a current project, and I am not sure if the current approach is the best/most idiomatic way to do it.

So far I have a parent object which has a shared state with some child objects, and the state is being shared as a CurrentValueSubject:


class Child {
    var state: CurrentValueSubject<SharedState, Never>

    init(_ state: CurrentValueSubject<SharedState, Never>) {
        self.state = state
        state.sink { value in self.doSomething(value) }
    }
}

class Parent {
    var state: CurrentValueSubject<SharedState, Never> = 

    let child1: Child
    let child2: Child

    init() {
        let state = CurrentValueSubject<SharedState, Never>(SharedState())
        self.state = state
        self.child1 = Child(state: state)
        self.child2 = Child(state: state)
    }
    ...
}

So the goal is that the parent, child1 and child2 should each be able to update the shared state, and the other components should be able to observe and respond to the value change.

I believe the current implementation should work, but is there a better/more idiomatic way to implement this?

note: I this is not in the context of SwiftUI

1 Answers

Keep in mind that Combine builds pipelines and pipelines allow data to flow in one way. That means that a child can observe changes out of a combine pipeline, but to send changes back up, you need another path.

The fact that the Parent implements the state using a CurrentValueSubject is an implementation detail of the parent. Passing it along to the children gives you a "leaky abstraction". You're letting implementation details of the parent "leak" out to others.

My preference would be to give the parent control over how it's values get set. I like for the parent to hand children something that allows it to keep control over how the state is updated, but still lets the child make changes. Combine coms from the tradition of functional programming so I might solve the problem by having the parent pass a function to the children. For example:

import UIKit
import Combine

class Child {
    let observe: AnyPublisher<Int, Never>
    let update: (Int) -> Void

    init(observe: AnyPublisher<Int, Never>,
         update: @escaping (Int)->Void) {
        
        self.observe = observe
        self.update = update
    }
}

class Parent {
    let state = CurrentValueSubject<Int, Never>(0)
    
    func createChild() -> Child {
        Child(observe: state.eraseToAnyPublisher()) {
            state.send($0)
        }
    }
}

Here, the fact that the parent implements its property using a CurrentValueSubject is hidden. For learning when the value changes, you get an AnyPublisher. If you want to make a change then you get a function that will change the value. The implementation details of how the Parent wants you to change that value is encapsulated inside the function.

When you create the child, you send both the downstream and upstream channels.

Related