The Pub-Sub Pattern

The Publish-Subscribe Pattern in Software Architecture is a messaging pattern in which senders of messages, called Publishers do not send messages to specific receivers, and have no knowledge of the receivers, called Subscribers. Subscribers are free to subscribe to any publisher to listen to and receive messages that the publishers broadcast.

This forms the core concept behind the Combine Framework.

Combine

The fundamental building blocks that constitute Combine are:

  • Publishers
  • Subscribers
  • Operators

Let’s go over each of this in a bit more detail.

Publishers

Publishers declare a type that can publish values over time.
These are objects that can be subscribed to and define an asynchronous event stream.
There are four kinds of messages Publishers are able to transmit:

  1. Subscription

A connection between a publisher and a subscriber.

2.  Value

The standard behaviour of a publisher we’re typically interested in, is its ability to publish useful data. These can be any kind of values that a subscriber might be designed to receive.

3. Error

The Publisher could also transmit an error, when there’s one. A Subscriber could then respond accordingly when it encounters an error.

This is represented as:

.failure(e)

4. Completion

The completion is an optional signal that a publisher could transmit to indicate that the stream has ended successfully and that no more data will be transmitted.

This is represented as:

.finished

Both .failure and .finished are terminal messages which indicate that the stream is no longer transmitting messages. The subscribers are expected to be designed to handle these cases.

Publishers are typically described by two attributes:

  • Output: The kind of values the published by the publishers.
  • Failure: The kind of errors the publisher might publish

In Combine, Publishers are standard Swift protocols and the attributes that describe them are denoted using associated types —

protocol Publisher {
	associatedtype Output
	associatedtype Failure
	
	func subscribe<S: Subscribe>(_ subscribe: S) {}
}

Publishers also describe how to attach subscribers to themselves as long as the subscribers Input and Failure type match the Publisher’s Output and Failure type. Which is understandable if we’re designing subscribers to listen to a certain kind of publisher.

So, in short any publisher can be denoted as –

PublisherName<Output, Failure>

Subscribers

Subscribers declare a type that can receive an input from a publisher. They’re described by two attributes:

  • Input: The kind of values it can receive
  • Failure: The kind of errors it can receive

Describing a subscriber is again as simple as —

SubscriberName<Input, Failure>

Subscribers have three key functions:

  • Receive a subscription
  • Receive an input (value from a publisher)
  • Receive a terminal signal, completion (incase of finite publishers) or a Failure.

Subscribers act and mutate state on the values that they receive from the publishers, because of which they’re reference types by which I mean they’re classes.

Data Flow

A Publisher is responsible for transmitting data to a subscriber, but only after a subscription is obtained by the subscriber.

The pattern may usually include an operator in between which makes for two kinds of streams to be possible:

  • Upstream: Stream of data from a publisher
  • Downstream: Stream of data to a subscriber

If we recall from the previous post, the data flow between the publishers and subscribers is nothing but a stream of values —

With this in mind, let’s look at the pattern that’s used in establishing this communication.

The Pattern

Let’s look into how the communication between the publishers and subscribers take place in Combine —

The Pattern of Communication

It’s typically a 4 step process. The initial setup might consist of an object holding a subscriber which intends to listen to a publisher —

Step I: The object holding the subscriber calls the subscribe(_:) method to request for a subscription.

Step II: The publisher then sends a subscription to the subscriber, and the subscriber is notified by[receive(subscription: Subscription)]

Step III: The Subscriber then sends in a demand request for n values to the publisher.

Step IV: The Publisher now sends the n or n-1 values to the subscriber.

If the Publisher is finite, after the values are sent, a completion signal is sent or a failure signal in the event of an error.


We'll discuss a bit more on Operators and Subjects in the next post.