Question

I am just getting started with RxJava, Java's implementation of ReactiveX (also known as Rx and Reactive Extensions). Something that really struck me was the massive size of RxJava's Flowable class: it has 460 methods!

To be fair:

  • There are a lot of methods that are overloaded, which bumps the total number of methods significantly.

  • Perhaps this class should be broken up, but my knowledge and understanding of RxJava is very limited. The folks who created RxJava are surely very smart, and they can presumably offer valid arguments for choosing to create Flowable with so many methods.

On the other hand:

  • RxJava is the Java implementation of Microsoft's Reactive Extensions, and that does not even have a Flowable class, so this is not a case of blindly porting an existing class and implementing it in Java.

  • [Update: The previous point in italics is factually incorrect: Microsoft's Observable class, which has over 400 methods, was used as the basis for RxJava's Observable class, and Flowable is similar to Observable but handles backpressure for large volumes of data. So the RxJava team were porting an existing class. This post should have been challenging the original design of the Observable class by Microsoft rather than RxJava's Flowable class.]

  • RxJava is only a little over 3 years old, so this is not an example of code being mis-designed due a lack of knowledge about good (SOLID) class design principles (as was the case with early releases of Java).

For a class as big as Flowable its design seems inherently wrong, but maybe not; one answer to this SE question What is the limit to the number of a class methods? suggested that the answer is "Have as many methods as you need".

Clearly there are some classes that legitimately need a fair number of methods to support them regardless of language, because they don't readily break down into anything smaller and they have a fair number of characteristics and attributes. For example: strings, colors, spreadsheet cells, database result sets and HTTP requests. Having perhaps a few dozen methods for classes to represent those things doesn't seem unreasonable.

But does Flowable truly need 460 methods, or is it so huge that it is necessarily an example of bad class design?

[To be clear: this question specifically relates to RxJava's Flowable class rather than God objects in general.]

Was it helpful?

Solution

TL;DL

The lack of language features of Java compared to C# as well as discoverability considerations made us put source and intermediate operators into large classes.

Design

The original Rx.NET was developed in C# 3.0 which has two crucial features: extension methods and partial classes. The former lets you define instance methods on other types which then appear to be part of that target type whereas partial classes allows you to split large classes into multiple files.

Neither of these features were or are present in Java, therefore, we had to find a way to have RxJava usable conveniently enough.

There are two kinds of operators in RxJava: source-like represented by static factory methods and intermediate-like represented by instance methods. The former could live in any class and thus they could have been distributed along multiple utility classes. The latter requires an instance to be worked on. In concept, these all could be expressed via static methods with the upstream as the first parameter.

In practice, however, having multiple entry-classes makes feature discovery by new users inconvenient (remember, RxJava had to bring a new concept and programming paradigm into Java) as well as usage of those intermediate operators somewhat of a nightmare. Therefore, the original team came up with the so-called fluent API design: one class that holds all the static and instance methods and represents a source or processing stage all by itself.

Due to the first class nature of errors, concurrency support and functional nature, one can come up with all sorts of sources and transformations regarding the reactive flow. As the library (and concept) evolved since the days of Rx.NET, more and more standard operators were added, which by nature increased the method count. This lead to two usual complaints:

  • Why are there so many methods?
  • Why isn't there a method X that solves my very particular problem?

Writing reactive operators is hard task not many people mastered over the years; most typical library users can't create operators by themselves (and often it is not really necessary for them to try). This means we, from time to time, add further operators to the standard set. In contrast, we have rejected many more operators due to being too specific or simply a convenience that can't pull its own weight.

I'd say the design of RxJava grew organically and not really along certain design principles such as SOLID. It is mostly driven by usage and feel of its fluent API.

Other relations to Rx.NET

I joined the RxJava development in late 2013. As far as I can tell, the initial early 0.x versions were largely a black-box reimplementation where the names and signatures of Rx.NET Observable operators as well as a few architectural decisions were reused. This involved about 20% of the Rx.NET operators. The main difficulty back then was the resolution of language and platform differences between C# and Java. With great effort, we managed to implement many operators without looking at the source code of Rx.NET and we ported the more complicated ones.

In this sense, up until RxJava 0.19, our Observable was equivalent to Rx.NET's IObservable and its companion Observable extension methods. However, the so-called backpressure problem appeared and RxJava 0.20 started to diverge from Rx.NET on a protocol and architectural level. The available operators were exteneded, many became backpressure-aware and we introduced new types: Single and Completable in the 1.x era, which have no counterparts in Rx.NET as of now.

Backpressure-awareness complicates things considerably and the 1.x Observable received it as an afterthought. We swore allegiance to binary compatibility so changing the protocol and API was mostly not possible.

There was another problem with Rx.NET's architecture: synchronous cancellation is not possible because to do that, one needs the Disposable to be returned before the operator starts executing. However, sources such as Range were eager and don't return until they finish. This problem can be solved by injecting a Disposable into the Observer instead of returning one from subscribe().

RxJava 2.x was redesigned and reimplemented from scratch along these lines. We have a separate, backpressure-aware type Flowable which offers the same set of operators as Observable. Observable does not support backpressure and is somewhat equivalent to Rx.NET's Observable. Internally, all reactive types inject their cancellation handle to their consumers, allowing synchronous cancellation to work efficiently.

OTHER TIPS

While I'll admit I'm not familiar with the library, I took a look at the Flowable class in question and it seems like it's acting like a hub of sorts. In other words, it is a class meant to validate input and distribute calls accordingly in the project.

This class therefore would not really be considered a God object as a God object is one that attempts to do everything. This does very little in terms of logic. In terms of single-responsibility, the class's only job could be said to be delegating work throughout the library.

So naturally such a class would require a method for every possible task you'd require of a Flowable class in this context. You see the same type of pattern with jQuery library in javascript, where the variable $ has all the functions and variables necessary to perform calls on the library, albeit in the case of jQuery, the code is not simply delegated but also a good bit of logic is run within.

I think you should be careful to make a class like this, but it has its place so long as the developer remembers that it is only a hub, and therefore doesn't slowly convert to a god object.

.NET's RX's equivalent to Flowable is Observable. It also has all of those methods, but they are static and usable as extension methods. The main point of RX is that the composition is written using fluent interface.

But for Java to have fluent interface, it needs those metods to be instance methods, as static methods wouldn't compose nicely and it doesn't have extension methods to make static methods composable. So in practice, all of those methods could be made static methods, as long as you were fine not using fluent interface syntax.

Licensed under: CC-BY-SA with attribution
scroll top