Architecturing an App with Functional Reactive Programming

Publié dans Coder stories

02 mai 2019

17min

Architecturing an App with Functional Reactive Programming
auteur.e
Vincent Pradeilles

iOS developer @ equensWorldline

Whenever a team is about to start a new project, there’s one question that always comes up: “Which architecture are we going to follow?” And every developer tends to have their own opinion on the matter, which makes perfect sense, because it’s such an important topic.

Think about it: Good architectures result in apps that perform well, are easy to maintain, and are fun to work on. On the other hand, a subpar architecture usually ends up delivering the kind of glitched and clunky project that people neither want to use or maintain.

Here’s our feedback on one of the highest-trending types of architectures: Functional reactive architecture.

Functional, Reactive?

The term functional reactive architecture definitely feels like a mouthful. And it should, because it encompasses two very broad concepts of software programming: Functional programming and reactive programming. So let’s start with a quick recap of these two concepts.

To sum up functional programming in a single sentence—which is hard!—we could say that it is a programming paradigm that relies on pure functions. A function is said to be pure if, 1) it has no side effects, meaning that it only reads data from its arguments and only outputs data through its return value; and 2) it always returns the same value when given the same set of arguments. To give some counterexamples, functions that read or write from a global variable, or that rely on randomness, do not qualify as pure functions.

Pure functions are deemed particularly desirable because their reliable and predictable behavior allows developers to easily reason about them.

Now, to explain the reactive part, we think it’s best to start with a definition: A program is said to be reactive if it explicitly subscribes to future events in order to process them once they have been emitted.

If we try to reformulate this definition, we could say that reactive programs are meant to deal with event flows, using an approach whereby the consumer is responsible for both subscribing to new events and defining how they should be processed.

While this might be the first time you’ve heard about this concept, there is actually a very good chance that you have unknowingly already taken advantage of it.

Consider the following JavaScript snippet:

$("#btn_1").click(function() {  alert("Btn 1 Clicked");});

This code uses jQuery to subscribe to some user inputs (here, clicks on a button) and it defines how those events should be processed (here, by triggering a pop-up).

As we just saw, reactive programming is a very intuitive way to deal with asynchronous events. But it comes with a major flaw: It processes events using callbacks. Now, don’t misunderstand us, there’s nothing wrong with callbacks in themselves. However, when we start to rely heavily on them, here’s the kind of code we quickly end up with:

const verifyUser = function(username, password, callback){   dataBase.verifyUser(username, password, (error, userInfo) => {       if (error) {           callback(error)       } else {           dataBase.getRoles(username, (error, roles) => {               if (error) {                   callback(error)               } else {                   dataBase.logAccess(username, (error) => {                       if (error) {                           callback(error);                       } else {                           callback(null, userInfo, roles);                       }                   })               }           })       }   })};

This is often referred to as “callback hell”: A situation where callbacks are being nested to the point where it takes a tremendous effort to simply understand what is going on.

The goal of a functional reactive architecture is therefore to enable the flexibility of reactive programming while relying on the principles of functional programming in order to structure the code in a viable manner. If we build upon the previously stated definition, we could say that a functional reactive program explicitly subscribes to future events, in order to process them once they have been emitted, in a scalable and maintainable way.

Let’s look at a concrete use case

Now that we have some working knowledge of what functional reactive programming is, let’s focus on how it can be applied in a project.

The first thing to do is to select a framework that already implements all the basic building blocks. There are several choices out there, but for the purposes of this article we are going to focus on ReactiveX (Rx). Rx has the advantage of providing several specific language implementations (RxJava, RxJS, RxSwift, etc.) of the same application programming interface (API). In this article, we will mainly use RxSwift and RxKotlin to provide examples, but thanks to the API standardization, everything you learn here will be easily applicable in your favorite language.

To give you an understanding of how Rx works, we are going to go over its three most important types.

The first one is called Observable, which can be thought of as an event producer:

// Swiftclass Observable<Value> {    public func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.Value == Value}

Now that we have a producer, we naturally also need a consumer, which is called an Observer:

// Swiftprotocol ObserverType {    associatedtype Value        func on(_ event: Event<Value>)}

As we can infer from the code, an Observer provides a function that consumes an Event:

// Swiftenum Event<Value> {    case next(Value)    case error(Swift.Error)    case completed}

There are three flavors of events: They can indicate that a new value has been produced, they can report that an error has occurred, or they can state that a sequence of events is now over.

When it comes to RxKotlin, the same concepts are implemented in a slightly different manner. If you are familiar with the language, you’ll definitely notice that the code below is far from idiomatic. The reason behind this is quite interesting: RxKotlin is actually built on top of RxJava, so all its basic building blocks have been “translated” from Java.

// Kotlinabstract class Observable<Value> {   fun subscribe(observer: Observer<Value>): Disposable}interface Observer<Value> {   fun onNext(value: Value);   fun onError(error: Throwable);   fun onComplete();}

Now that we understand how Rx works, let’s try to see how we can use it to build a layered architecture. To keep the code short and to the point, I’m going to assume we have at our disposal the standard libraries and tools of each language. First, we are going to implement an HTTP client:

// Swiftclass Client {    func requestJSON(_ request: URLRequest) -> Observable<JSON> {        return URLSession            .shared            .rx            .json(request: request)    }}

What we are doing here is implementing a function that performs an HTTP request to retrieve a JSON document. But instead of relying on a raw callback to communicate the result, we return an Observable, which we will then use to manipulate the result of the request in a structured manner.

In Kotlin, we don’t need to write an HTTP client, as Retrofit will automatically generate one from us, using the annotation we’ll define in our servicing layer.

Once we have this basic HTTP client, the next step is to implement a web service. Here’s what it could look like:

// Swiftclass MyService {    struct MyServiceParameters: Encodable { /* ... */ }    struct MyServiceResponse: Decodable { /* ... */ }    private let client = Client()    func call(with parameters: MyServiceParameters) -> Observable<MyServiceResponse> {        return client.requestJSON(parameters.encode())            .map { json in return json.decode(MyServiceResponse.self) }    }}

To simplify the code, we are assuming that the type MyServiceParameters holds all the necessary data to perform an HTTP call, and provides an encode() method that serializes this data in the form of a URLRequest. We also assume that the type MyServiceResponse defines the necessary mapping to decode the JSON document returned by the web service.

The important line to focus on is this: .map { json in return json.decode(MyServiceResponse.self) }. What’s happening here is that we are using the function map()—a well-known primitive of functional programming—to transform the result of the HTTP call.

The nice thing is that this transformation is written in a very elegant way: We are only required to provide the function that performs the transformation. Everything else is being taken care of for us—there’s no need to worry when or how this function should be called. It’s a perfect example of separation of concerns.

Moreover, the transformation function falls within the category of what we previously defined as pure functions. This means that it can be easily factored out and reused throughout our code, without any fear of unexpected side effects. It also means that this function is very easy to test, because it does not rely on any hidden state.

// Kotlindata class MyServiceResponse( /* ... */)interface MyService {    @FormUrlEncoded    @POST("/myservice/endpoint")    fun call(@Header("Authorization") authorizationHeader: String,         @Field("first_argument") firstArgument: String, @Field("second_argument") secondArgument: Int ): Observable<MyServiceResponse>}

In Kotlin, the general idea is the same. As you can see, the code is shorter, because we are able to integrate more closely with some standard tools: We use annotations from Retrofit to indicate how the HTTP request must be constructed. We also assume that the response type MyServiceResponse defines the appropriate @SerializedName annotations to allow its decoding by the library Gson.

After defining a working web service, the next step is to implement some business logic. We are going to achieve this by implementing a ViewModel.

// Swiftclass MyViewModel {    let presentableData = PublishSubject<String>    private let service = MyService()    private let disposeBag = DisposeBag()    private func format(_ response: MyServiceResponse) -> String { /* ... */ }    func fetchData() {        let parameters = MyServiceParameters()        service.call(with: parameters)            .map { response in return self.format(response) }            .subscribe(onNext: { [weak self] presentableData in                self?.presentableData.onNext(presentableData)            })            .disposed(by: self.disposeBag)    }}

In this class we are using an instance of MyService to make a call to a web service. Then we use the static function format() to perform some business logic—that is, to format the raw data in a way that makes it presentable to the user. Finally, we update the variable presentableData with the result of this process.

The type PublishSubject is nothing more than a convenience API over Observable: It provides the same API as an Observable, but it also lets the developer manually make it emit a new value.

If you’re wondering what the deal is with DisposeBag, the answer is actually quite simple: When we subscribe to an Observable, we trigger the allocation of some resources—here, we are performing networking calls, so sockets are probably being created. Consequently, we need a way to indicate when those resources must be freed. A dispose bag allows us to implement an elegant solution to this problem: Every object that subscribes to an Observable must add the subscription to its own dispose bag, then, when the object gets deallocated, all the subscriptions stored in its dispose bag also get deallocated.

// Kotlinclass MyViewModel {    val presentableData = PublishSubject.create<String>()    private val service = Retrofit.Builder()        .baseUrl("https://api.base.url/")        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())        .addConverterFactory(GsonConverterFactory.create())        .build()        .create(MyService::class.java)    private val disposables = CompositeDisposable()    companion object {        fun format(response: MyServiceResponse): String { /* … */ }    }    fun fetchData() {        service.call("Token", "FirstArgument", 0)            .map { MyViewModel.format(it) }            .subscribeBy(                onNext = { presentableData ->                    this.presentableData.onNext(presentableData)                }            ).addTo(disposables)    }}

On the Kotlin side of things, besides some differences in syntax, the code looks very similar. The only big difference lies in the process to instantiate our service, which this time is a bit more complex, but it’s a trade-off that comes with Retrofit—though bear in mind that we didn’t have to write an HTTP client by ourselves.

Finally, we want to implement a user interface that will display the result of our business logic. As the Swift and Kotlin implementation are this time extremely similar, separate explanations are no longer required.

// Swiftclass MyViewController: UIViewController {    let label = UILabel()    private let viewModel = MyViewModel()    private let disposeBag = DisposeBag()    override func viewDidLoad() {        super.viewDidLoad()        self.bindViewModel()        self.viewModel.fetchData()    }    private func bindViewModel() {        self.viewModel.presentableData            .asObservable()            .subscribe(onNext: { [weak self] presentableData in                self?.label.text = presentableData            })            .disposed(by: self.disposeBag)    }}
// Kotlinclass MainActivity : AppCompatActivity() {    val textView = findViewById<TextView>(R.id.myTextView)    private val viewModel = MyViewModel()    private val disposables = CompositeDisposable()    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        bindViewModel()        viewModel.fetchData()    }   private fun bindViewModel() {        viewModel.presentableData            .subscribeBy(                onNext = { presentableData ->                    textView.setText(presentableData)                }            ).addTo(disposables)    }}

As you can imagine, the important part of this class is the function bindViewModel(). Inside this function, the controller is going to subscribe to the variable that its view model exposes and it will use the value it produces to update its user interface.

What’s really nice is that, now, if we want to refresh the content of the screen, we only need to call viewModel.fetchData(), and we won’t have to preoccupy ourselves with manually updating every widget on the screen.

So what can we take away from this implementation of an architecture that leverages functional reactive programming? The first aspect we can note is that every type we had to create is focused on a single responsibility. This results in very concise and consistent code that will be easy to maintain over time.

We can also notice that the layers of code we drafted define a set of good practices that can be used to implement several features using a common convention, which will make a project feel like it was crafted from a single piece.

A direct consequence is that teamwork will be made easier, both because there will be clear guidelines on how to implement new features, and also the codebase will be organized in a way that will minimize the chance of running into conflicts while working in parallel on different features.

Beyond front-end development

If you go looking for tutorials on reactive programming, the chances are you will find content that, as with our own example, explains how to implement a front-end application. However, it would be a mistake to think that this is the only place where reactive programming can be leveraged. As it turns out, reactive programming is also a very powerful tool for implementing features on the back end!

A good example of this is given in this talk by a Netflix engineer: Reactive programming is an excellent tool for implementing the complex composition of several web services, and to give you an understanding of the reasons why, we need to introduce you to a couple of what Rx calls operators. You can think of them as functions that will enable you to compose two or more Observables.

The first of those operators is called flatMap and it allows you to compose sequential calls. Its main benefit is that it solves the problem of sequential composition without requiring any kind of callback nestings, resulting in code that is easier to read and maintain:

// Swiftfunc service1() -> Observable<Int>func service2(_ arg: Int) -> Observable<String>service1()    .flatMap { service1Result in return service2(service1Result) }    .subscribe(onNext: { service2Result in        print(service2Result)    }, onError: { error in        // deal with `error`    })
// Kotlinfun service1(): Observable<Int>fun service2(arg: Int): Observable<String>service1()        .flatMap { service1Result -> service2(service1Result) }        .subscribeBy( onNext = { service2Result ->                print(service2Result)        }, onError = { error ->        // deal with `error`    })

The second operator is called merge and it lets us compose parallel calls. Like flatMap, it does so with a very declarative API that abstracts away all the tricky business of a synchronisation logic:

// Swiftfunc service1() -> Observable<String>func service2() -> Observable<String>Observable.merge(service1(), service2())    .suscribe(onNext: {        print($0)    })
// Kotlinfun service1(): Observable<String>fun service2(): Observable<String>listOf(service1(), service2())    .merge()    .subscribeBy(onNext = { print(it) })

If we were to build a web service that manages products, these are the data types and API that could be at our disposal:

// Swiftstruct Product {    let id: Int    /*    Many other properties    */} // Retrieves an array of all the available product idfunc getProductIds() -> Observable<[Int]> // Retrieves the content of a product given its idfunc getProductDetails(_ productId: Int) -> Observable<Product>
//Kotlindata class Product (    val id: Int    /*    Many other properties         */)// Retrieves an array of all the available product idfun getProductIds(): Observable<Array<Int>>// Retrieves the content of a product given its idfun getProductDetails(productId: Int): Observable<Product>

As you can see, we have an API to retrieve all the existing product ids, and another one to retrieve the actual content of a product. Now imagine that we need to build a new API that returns the actual content of all existing products.

This kind of requirement is fairly common, so it’s very important that we have at our disposal a simple and reliable way to satisfy it. If we try to reason about the issue, we can understand that we first need to retrieve the list of all the product ids, then, for each product id, we need to get its actual content.

To reformulate this in the terms we previously defined, we need to compose both sequentials and parallels calls. This means that this is a very good example of seeing how flatMap and merge can be used.

Here is a possible implementation:

//Swiftfunc getAllProducts() -> Observable<Product> {    // 1    return getProductIds().flatMap { productIds -> Observable<Product> in        // 2        let productsDetailsCalls = productIds.map { productId in getProductDetails(productId) }                // 3        return Observable.merge(productsDetailsCalls)    }}
// Kotlinfun getAllProducts(): Observable<Product> {    // 1    return getProductIds().flatMap { productIds ->        // 2        val productsDetailsCalls = productIds.map { productId -> getProductDetails(productId) }        // 3        productsDetailsCalls.merge()    }}

Let’s look in more detail at what’s happening:

  • First, we call getProductIds(). Since we are going to use the result of this call as the input of the following calls, we use the operator flatMap to implement the sequential composition.
  • Then we iterate over the list of product ids using the function map, and for each product id, we perform a new call to get the content of the product.
  • Finally, we use the operator merge to synchronise all those parallel calls and assemble their respective results inside a single Observable.

As you can see, by using only a couple of rules about sequential and parallel calls, we have been able to derive a rather straightforward implementation that will positively contribute toward a maintainable codebase.

Making your project more reactive

So we’ve covered some ground now, and by this point you should have a good understanding of what a functional reactive architecture can bring to a project, as summarized below:

  • It offers a model to write asynchronous code that is both reliable and easy to maintain, thanks to its ability to manipulate and compose data streams in a declarative manner.
  • It forces us to organize our code in terms of producers and consumers, thus strengthening the separation of concerns and focusing our types toward single responsibilities.
  • It relies heavily on pure functions, which have the advantage of being simple to test and reuse.
  • It defines a set of good practices that effectively streamline collaboration and teamwork.

Now, how do you go about integrating it within a new or existing project?

First, you need to ask yourself which part of your project is a good candidate. As we saw, the purpose of a reactive architecture is to deal elegantly with complex asynchronous use cases. So, the natural choices are the parts that involve things like complex user interactions, data that needs to be regularly refreshed, or convoluted sequences of network calls.

What’s important to note is that a reactive architecture is not an “everything or nothing” approach. You are absolutely allowed to apply it when it makes sense, and use a completely different approach when it wouldn’t be solving any real issue.

It’s also worth mentioning that, while we chose to focus on the Rx API, many other implementations of functional reactive programming do exist. If you write JavaScript, you might have heard about Promise: This also offers an implementation of functional reactive programming, albeit a less powerful one than Rx.

Finally, you need to be aware that successfully working with reactive architectures is a skill in itself, and as such it needs to be taught and acquired. You will need to dedicate some time and effort in order to become proficient, and so will your teammates. Newcomers will also need to pick up the skill, so you’ll need to dedicate a training period within your onboarding process.

Conclusion

It’s a common saying in software engineering that there is no such thing as a “silver bullet,” meaning that the perfect approach doesn’t exist. What we have at our disposal are tools and, depending on the context, some of them will be more appropriate than others.

In our opinion, functional reactive programming is indeed a tool that does manage to solve real issues. It offers helpful constructs to tackle the tricky challenges posed by asynchronous code, along with standardized APIs that help enforce consistent coding conventions across large codebases.

Nonetheless, as with any tool, it must be used wittingly, by taking all the parameters into account. These include parts of the project where it will be most helpful, the most appropriate library to use with respect to the complexity of the requirements, and the needs it will generate in terms of skill acquirement.

That being said, functional reactive programming has been able to gain the favor of some of the biggest frameworks out there, so the chances are it will have a positive impact on your work if you give it a try!

This article is part of Behind the Code, the media for developers, by developers. Discover more articles and videos by visiting Behind the Code!

Want to contribute? Get published!

Follow us on Twitter to stay tuned!

Illustration by Blok

Les thématiques abordées