HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Usability of this Observable class

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
usabilityclassthisobservable

Problem

I created an Observable class that allows me to subscribe to a wrapped value. The implementation looks like this:

class Observable {
    typealias DidChangeHandler = (oldValue: ValueType?, newValue: ValueType) -> ()

    // MARK: Properties

    var value : ValueType {
        didSet {
            for (owner, handlers) in self._observers {
                for handler in handlers {
                    handler(oldValue: oldValue, newValue: value)
                }
            }
        }
    }

    // MARK: Initializers

    init(_ value: ValueType) {
        self.value = value
    }

    // MARK: Methods

    func addObserverForOwner(owner: AnyObject, triggerImmediately: Bool, handler: DidChangeHandler) {
        if let index = self._indexOfOwner(owner) {
            // since the owner exists, add the handler to the existing array
            self._observers[index].handlers.append(handler)
        }
        else {
            // since the owner does not already exist, add a new tuple with the
            // owner and an array with the handler
            self._observers.append(owner: owner, handlers: [handler])
        }

        if (triggerImmediately) {
            handler(oldValue: nil, newValue: self.value)
        }
    }

    func removeObserversForOwner(owner: AnyObject) {
        if let index = self._indexOfOwner(owner) {
            self._observers.removeAtIndex(index)
        }
    }

    // MARK: Private Properties

    private var _observers: [(owner: AnyObject, handlers: [DidChangeHandler])] = []

    // MARK: Private Methods

    private func _indexOfOwner(owner: AnyObject) -> Int? {
        var index: Int = 0
        for (possibleOwner, handlers) in self._observers {
            if possibleOwner === owner {
                return index
            }
            index++
        }
        return nil
    }
}


This allows me to create a variable and observer it as follows:

```
var observableString = Observable("String")

observableString.addObser

Solution

You worry about triggerImmediately being too verbose (which can be fixed), yet you have addObserverForOwner as the primary function name, and for me, this is the part that is a bit too verbose.

Since what we're doing with this is so similar to the existing KVO in Objective-C (which can also be used in Swift), we'd do well to make this look and feel like KVO.

The KVO method looks like this: addObserver:forKeyPath:options:context:. In KVO however, we don't get to pick our own handler. We just have to implement observeValueForKeyPath:ofObject:change:context:.

Now, I'm not going to recommend a direct copy. We can drop the forKeyPath: part of the first method. The general consensus with KVO also seems to be that context: is relatively useless (it's a value you send when you start observing, and it is sent back to you in the call back method). I also do like that you're suggesting the user can pick whatever method or function they want as the handler.

But, let's make some changes, shall we?

First of all, let's change our handler. Instead of a method that takes an oldValue and a newValue, let's use a method that just takes a dictionary, shall we?

We're doing this, because we should change triggerImmediately to options, and provide an enum that lets users build a bitmask for their options. In KVO, we have the following options:

  • NSKeyValueObservingOptionNew



  • NSKeyValueObservingOptionOld



  • NSKeyValueObservingOptionInitial



  • NSKeyValueObservingOptionPrior



The first two are self explanatory.

NSKeyValueObservingOptionInitial is a flag that say the handler block should be called before the addObserver method is finished--we send the current value of the observable to the handler block as a "new" value.

NSKeyValueObservingOptionPrior means the user wants to know about the values just before they're actually changed. In the case of Swift, this means we'd fire in willSet with this option on (but we also still fire in didSet).

I'm not suggesting you use the NSKeyValueObservingOption enum. I suggest you create your own enum. As a minimum, include these options, but you may want to include others as well at some point in the future. Maybe a StopObservingAfterChange option that automatically removes the observer after the first didSet for example.

But anyway, about our handler. It should just take a dictionary argument as I stated earlier. What values it contains will depend on the scenario and which bits are set in the options.

If the new bit is set, the dictionary will always hold the new value. If the old bit is set, it will hold the old value in every situation it can (i.e. not when the initial bit is set and we fire before the addObserver method returns).

If the prior bit is set, when we call the handler from willSet, we include another key: "prior" which holds a boolean value of true. I guess you could realistically include this key every time just set it to false in every other scenario.

Anyways, changing the add observer method to:

addObserver(observer:AnyObject, options:ObservableOptions, handler:DidChangeHandler);


Means that you can always add options in the future without screwing up any of the existing uses. And changing the handler to taking a dictionary means we throw all the values we want to send to the handler into the dictionary, and not only is the handler flexible for different current use cases, it is also flexible for all future use cases.

As a final note, I might include constant values to use as the keys to the "change" dictionary.

Code Snippets

addObserver(observer:AnyObject, options:ObservableOptions, handler:DidChangeHandler);

Context

StackExchange Code Review Q#58220, answer score: 4

Revisions (0)

No revisions yet.