/RVS_GeneralObserver

A General-Purpose Observer/Observable Infrastructure

Primary LanguageSwiftMIT LicenseMIT

Icon

RVS_GeneralObserver

DESCRIPTION

This is a general-purpose set of protocols that are designed to provide a simple infrastructure for a basic Observer implementation.

Here is the GitHub repo for the project.

Here is the main documentation for the project.

Does Not Handle Messaging

This does not deal with messaging or managing communications between observers and observables. It simply gives them the infrastructure to track each other.

There are a few callbacks, explicitly related to aggregate management, but, otherwise, it's simply a tool to provide a reliable relationship graph management.

It is up to implementations to handle what to do with this information.

Protocol-Based

This is based on protocols, as opposed to classes or structs. A couple of the protocols require that they be implemented as classes. There is heavy reliance on protocol default implementations to deliver the infrastructure.

WHAT PROBLEM DOES THIS SOLVE?

At its heart, any observer implementation is really just a relationship graph. Observers subscribe to Observables. Observables use the subscription as a one-way broadcast medium.

Managing the subscriptions and relationships is absolutely fundamental to the pattern. If we can't trust our subscription list, then everything built upon it is at risk.

Different From Delegate

Apple uses the Delegate pattern in a lot of their Cocoa infrastucture. This is an excellent and simple pattern that has the following features:

  • Delegates are a "one-to-one" relationship. A class that has a delegate has only one delegate.
  • Delegates are often "two-way" relationships. Delegates can send information back to the objects to which they are subscribed. In fact, the Data Source pattern codifies this explicitly.
  • Delegates require that all involved entities be classes. In fact, delegates need to derive from NSObject.

Observers (at least, they way I do them) have a different aspect:

  • They are usually a "one-to-many" relationship. Observers subscribe to observables, who are responsible for managing a list of subscribers.
  • They are a "one-way" relationship. Observables can only send messages to subscribers. They cannot receive anything. If an observable wants to get messages from a subscriber, then the subscriber needs to become an observable, and the old observable needs to subscribe to them.
  • It isn't a requirement for observers and observables to be classes, to satisfy the pattern, but I do require that a couple of the protocols be class-based protocols, in order to implement some of the defaults.

Benefits

Managing the subscription lists and relationships is a very fundamental part of Observer, and something that needs to be rock-solid. That was why this tool was developed.

We shouldn't even be thinking about this.

IMPLEMENTATION

The URI for the repo is:

Once you have the package included in your project (if you want to find out more about SPM, then you might want to view this series), you'll need to include the library. It will be a static (build-time) library:

import RVS_GeneralObserver

You can include the library by adding the following line to your Cartfile:

github "RiftValleySoftware/RVS_GeneralObserver"

You should probably just include the Carthage/Checkouts/RVS_GeneralObserver/Sources/RVS_GeneralObserver/RVS_GeneralObserver_Protocols.swift file directly, as opposed to building a library.

Git Submodule or Direct File Download

If you want to include the project as a submodule, simply use one of the URIs above (in the Swift Package Manager section). It's probably best to include the Sources/RVS_GeneralObserver/RVS_GeneralObserver_Protocols.swift file directly from the submodule (with no module import).

If you want to simply download and include the file, then there is only one file to deal with. The Sources/RVS_GeneralObserver/RVS_GeneralObserver_Protocols.swift file.

Just download and include that one file. No need to import a module.

Observables

Once that is done, you can make a class (it needs to be a class) Observable, by conforming to the RVS_GeneralObservableProtocol protocol.

You will need to create two stored properties in your implementation (the following examples are from the unit tests):

class BaseObservable: RVS_GeneralObservableProtocol {
    /* ############################################################## */
    /**
    The required UUID. It is set up with an initializer, and left alone.
    */
    let uuid = UUID()
    
    /* ############################################################## */
    /**
    This is the required observers Array.
    */
    var observers: [RVS_GeneralObserverProtocol] = []

The uuid property is a "set and forget" property. Simply do exactly as above, and never worry about it afterwards.

The observers Array is also one you're unlikely to use directly (but you'll probably cast it). It is where subscribers are tracked. This is how your observable will find broadcast targets. Normally, you'll probably cast this to an Array of more specific classes, like so:

var castArray: [MySpecificSubscriberThatConformsToRVS_GeneralObserverProtocol] { observers as? [MySpecificSubscriberThatConformsToRVS_GeneralObserverProtocol] ?? [] }

Observers

We have two types of Observers. One is a "generic" one, that can be applied to structs and classes, that does not track the Observables to which an Observer is subscribed, and the other is a class-only variant that tracks an Observer's subscriptions:

These examples are also from the unit tests:

Standard (struct or class) Observer:

struct BaseObserver: RVS_GeneralObserverProtocol {
    /* ############################################################## */
    /**
     The required UUID. It is set up with an initializer, and left alone.
     */
    let uuid = UUID()

class-only Subscription-Tracking Observer:

class SubTrackerObserver: RVS_GeneralObserverSubTrackerProtocol {
    /* ############################################################## */
    /**
     The required UUID. It is set up with an initializer, and left alone.
     */
    let uuid = UUID()
    
    /* ############################################################## */
    /**
     This is where we will track our subscriptions.
     */
    var subscriptions: [RVS_GeneralObservableProtocol] = []

Because the protocol default works with an Array of references, this should be a class.

Subscribing to an Observable is as simple as calling its subscribe() method, with the observer, supplied:

let subscribedObserver = observableInstance.subscribe(observerInstance)

The response is the observerInstance, if the subscription was successful. This allows the method to be chained. It may be nil, if the observer is already subscribed.

Unsubscribing is exactly the same, except that we call the unsubscribe() method, this time.

let unsubscribedObserver = observableInstance.unsubscribe(observerInstance)

There are also unsubscribeAll() methods for the Observable, and for the subscription-tracking Observer.

Calling these will remove every Observer from an Observable instance, or every Observable from an Observer instance.

Callbacks

There aren't any required callbacks in the protocols, but there are a few, very basic optional ones.

The Observer protocol has callbacks that are made at the time that a subscription is confirmed:

func subscribedTo(_ observable: RVS_GeneralObservableProtocol)

and when an unsubscription is confirmed:

func unsubscribedFrom(_ observable: RVS_GeneralObservableProtocol)

The subscription-tracking protocol has a couple of internal methods that aren't supposed to be used by conformant instances.

The Observable protocol has a single optional callback:

func observer(_ observer: RVS_GeneralObserverProtocol, didSubscribe: Bool)

This is called whenever an Observer subscribes or unsubscribes (the second argument indicates that).

Once you have set up the classes (or structs), you can then use the observers property (Observable) or the subscriptions property (the subcriber-tracking variant of the Observer protocol) to access and interact with the various targets, recasting, as necessary.

All protocols have an amISubscribed() Boolean method, where you pass in an Observer (or Observable) instance to be tested to see if an Observer is subscribed to an Observable.