After explaining for several years what reactive programming is, I have found that the best way to do it is to start with the interfaces that describe how the reactive programming works.
I assume you are familiar with these two areas (if not, follow the links and read the related article before continuing reading this one):
1) Java 8 streams (technically, they have nothing to do with reactive programming, but the idea of a data stream, processed by the processing pipeline of operations, connected in a fluent (dot) style, is very similar to the style used in reactive programming too);
2) Java functional programming (that is a must-to-know topic; without it, the reactive code loses a lot of its luster and you would not be able to understand reactive code written by others).
With that, let us look at the Flow class introduced in Java 9. It has four interfaces that support reactive streams specification:
public static interface Publisher<T> {
public void subscribe(Subscriber<T> subscriber);
}
public static interface Subscriber<T> {
public void onSubscribe(Subscription subscription);
public void onNext(T item);
public void onError(Throwable throwable);
public void onComplete();
}
public static interface Subscription {
public void request(long n);
public void cancel();
}
public static interface Processor<T,R>
extends Subscriber<T>, Publisher<R> {
}
In principle, you can know everything that can be learned about reactive programming from studying and thinking about these four interfaces.
You are probably familiar with publisher-subscriber concept. If not, you can understand it just intuitively: a publisher places a message in some specific place on specific topic, and the subscriber of this topic gets the message from that place.
You can see the echo of this approach in the above four interfaces too. The difference can be summarized in three bullet points:
— in reactive programming, the subscriber is not remotely located; instead, it is connected to the publisher directly: it is passed into the publisher as an object; which means there is no need to agree of some specific place where a message is placed, the message is passed just via a method call;
— there is a processor that can act as subscriber and the publisher at the same time, which allows building the processing pipeline: a processor subscribes to a publisher, then acts as a publisher for another processor, who subscribes to it, an so on, until a subscriber (which does not implement Publisher interface) subscribes to the processor;
— a subscription serves as a topic but is hidden from the application programmer and acts as an implementation detail – a mechanism that allows publisher and subscriber talk to each other.
So, a typical processing pipeline looks as follows:
publisher calls processorA
calls processorB
...
calls processorX
calls subscriber
While writing code, a programmer constructs such pipelines
Please, don’t, because the libraries that have implemented these and similar interfaces (we will talk about them in the following installments) did a good job by allowing to connect these calls using fluent (dot) style. The trick is to return a publisher object, and that is why Processor extends both interfaces – Subscriber and Publisher. That said, we continue.
Every time a publisher wants to pass data to the subscriber, it calls method onNext(T item) and passes an item. If this “subscriber” is actually a processor and has its own subscriber, it does something with the received item and calls its subscriber to pass the result, and so on, until the final subscriber is reached. The result of such processing can be a side effect (something is displayed, for example) or actually, some value is produced and returned by the subscriber all the way to the original caller of the publisher.
If an error happened on the way, it is passed down to the final subscriber using method onError(Throwable throwable).
If there is no more data to process, the method onComplete() is called.
Such a model is called reactive because it reacts to the data pushed into the processing pipeline by the publisher. Note that in Java 8 streams, the data are pulled by the terminal operation (the subscriber’s cousin).
Naturally, the processing of one item has to happen fast enough to be able to process the next item. If not, backpressure grows and … we will talk about what can be done about it in the following installments on this topic.
If it feels complex, be assured that there are several libraries out there already mature enough to hide all this complexity. One of them is
See other posts on reactive programming.
Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.