Most articles on functional-reactive programming are limited to demonstrating the capabilities of a particular tool in a particular task and do not provide an understanding of how to use all the power within the whole project.
I would like to share design experience using functionally reactive programming for iOS. It does not depend on the selected tool, be it
RAC ,
RxSwift ,
Interstellar or something else. This also applies when developing for MacOS.
At certain points I will write using Swift + RAC4, since these are my main tools at the moment. However, I will not use the terminology and features of RAC4 in the article.
')
Maybe you abandoned reactive programming in vain and it's time to start using it?
For a start, briefly about the myths among people who have only heard and heard about the reagent are not very good:
Myth 1 - The threshold for entering reactive programming is too high.
Nobody says that you need to use all available opportunities from the first minutes. You just need to understand the concept and basic basic operators (below I will write about 4 minimally necessary operators, and you will come to the rest as you solve various kinds of tasks).
You do not need to spend months and years on this, several days / weeks are enough (depending on the background), and if you have an experienced team, entry will be much faster.
Myth 2 - reagent is used only in the UI layer.
The reagent is convenient to use in business logic, and below I will show what we will get from this.
Myth 3 - the reactive code is very difficult to read and disassemble.
It all depends on the level of written code. With proper separation of the system, this increases the understanding of the code.
Moreover, it is not much more difficult than using a lot of kalbek.
And you can almost always write unreadable code.
Reagent concept
The process of writing a reactive code is similar to a children's trickle game. We create a path for water and only then we launch water. Water in our case is the calculation. Network request, getting data from the database, getting coordinates and many other things. The path for water in this case is signals.
So, the signal is the main building block containing some calculations. Above the signals, in turn, certain operations can be performed. By applying the operation to the signal, we get a new signal, which includes previous configurations.
By applying signal operations and combining with other signals, we create a data stream for calculations (dataflow). All this thread starts its execution at the moment of subscribing to a signal, which is
similar to lazy calculations. This makes it possible to more finely control the start time of the execution and subsequent actions. Our code is divided into logical parts (which increases readability), and we get the opportunity to create new “methods” literally “on the fly”, which increases the reusability of the code in the system. Looks like a higher order function, isn't it?
For the first time, the minimum required operations for configuring dataflow should be
mapping ,
filter ,
flatMap and
combineLatest .
And finally, a small feature, dataflow, is
data + errors , which makes it possible to describe a sequence of actions in 2 directions.
This is the minimum necessary theory.
Reagent and modular architecture
Take
SOA as an example, but of course this does not limit you in any way.
The scheme used may differ from others or be the same; I do not pretend to be a standard, just as I do not pretend to be universal. In most of our tasks, we adhere to this solution, hiding behind the service the data processing (and this does not necessarily have to be network requests).
Transport
So this is our first challenger for reactivity. Therefore, I will focus on this place in more detail.
First, let's take a look at typical solutions to this problem:
Use of 2 kalbektypealias EmptyClosure = () -> () func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> NSURLSessionTask
Using 1st kalbek typealias Response = (data: NSData?, code: Int) typealias Result = (response: Response, failed: NSError?) func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> NSURLSessionTask
Both solutions have their advantages and disadvantages. Consider both solutions within the scope of the task: show the loader for the duration of the network request.
In the first case, we clearly separate the actions to the success and failure of the action, however, we will duplicate the code indicating the completion of the action.
In the second case, we don’t need to duplicate the code, but we mix everything into one pile.
And you also need the ability to cancel the network request, and + you would need to encapsulate the work of the Transport.
Most likely, in this case, our code will look like
like this: protocol Disposable { func dispose() } typealias Response = (data: NSData?, code: Int) typealias Result = (response: Response, failed: NSError?) func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> Disposable? ... ... ... typealias EmptyClosure = () -> () func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> Disposable?
Now look at the solution using signals func getRequestJSON(urlPath: String, parameters: [String : String]) -> SignalProducer<Response, NSError> { return SignalProducer<Response, NSError> { observer, disposable in let task = ... { observer.sendNext(data: data, code: code) observer.sendCompleted()
Earlier, I deliberately missed one important point - when creating a signal, we not only write what to do when subscribing to a signal, but also what to do when the signal is canceled.
Signing to a signal returns an instance of the class Disposable (not written above, more), which allows you to cancel the signal.
Example code let disposable = getRequestJSON(url, parameters: parameters) // .startWithNext { data, code in ... ... ... } // startWithNext disposable.dispose() //
Now the callee can easily postpone the execution of the request, combine the result with other requests, write some actions on the signal events (from the example above to complete the request), as well as what to do when receiving data and what to do when an error occurs.
But before demonstrating such a code, I would like to talk about such a concept as
Side effect
Even if you did not come across this concept, then 100% of it was observed (or you came here by chance).
In simple terms, this is when our computational flow depends on its environment and changes it.
We try to write signals as a separate part of the code, thereby increasing its possible re-usability.
However, side effects are sometimes necessary and there is nothing terrible about it. Consider in the figure how we can use Side Effect in reactive programming:
Very simple, isn't it? We wedge between the execution of signals and perform certain actions. In essence, we perform actions on certain signal events. But at the same time, we keep the signals still clean and ready for reuse.
For example, from a previously created task: “Show loader at start of signal and remove at completion”.
Parsing
Recall a typical situation - the data from the server either came in the correct or in the wrong format. Solutions:
1) Kalbeki "data + error"
2) Apple's approach using NSError + &
3) try-catch
And what can the reagent give us?
Let's create a signal in which we will parse the response from the server and issue the result in certain events (next / failed).
Using the signal will make it possible to more clearly see the operation of the code + combine work with the network request signal. But is it worth it?
Example class ArticleSerializer { func deserializeArticles(data: NSData?, code: Int) -> SignalProducer<[Article], NSError> { return SignalProducer<[Article], NSError> { observer, _ in ... ... ... } }
Services
Combine the network request, parsing and add the ability to save the result of parsing in the
DAO .
code example class ArticleService { ... ... ... func downloadArticles() -> SignalProducer<[Article], NSError> { let url = resources.articlesPath let request = transport.getRequestJSON(url, parameters: nil) .flatMap(.Latest, transform: serializer.deserializeArticles) .on(next: dao.save) return request }
No nesting, everything is very simple and easy to read. Generally a very consistent code, is not it? And it will remain as simple, even if the signals will be executed on different threads. By the way, consider using combineLatest:
Synchronizing parallel queries userService.downloadRelationshipd() // .combineLatestWith(inviteService.downloadInvitation()) // + .observeOn(UIScheduler()) // ( ) .startWithNext { users, invitations in // }
It is worth noting that the code written above will not be executed until it is started by subscribing to the signal. In fact, we only indicate actions.
And now services have become even more transparent. They only interconnect parts of the business logic (including other services) and return the dataflow of these connections. A person using the received signal can very quickly add reaction to events or combine them with other signals.
And also ...
But all this would not be so interesting if it were not for a lot of operations on signals.
Establish a delay, repeat the result, various combining systems, setting the result to be received on a certain flow, convolutions, generators ... Just look at this
list .
Why did I not talk about working with UI and binding, which for many is the most "juice"?
This is a topic for a separate article, and there are already quite a lot of them, so I’ll just give a couple of links and finish
Better world with ReactiveCocoaReactiveCocoa. Concurrency. MultithreadingI have it all. Instead of a useless conclusion, I will leave a few practical conclusions from the last project:
1) Getting the Permission has proven itself as a good signal.
2) CLLocationManager behaved perfectly with signals. Especially the accumulation and editing of points.
3) It was also convenient to work with signals for such actions as: choosing a photo, sending an SMS and sending an email.