Holding back the imperatives

    Object-oriented paradigm is extremely convenient for business: it allows you to implement almost any idea, ensuring an acceptable performance of the product. In this case, the product we understand is the iOS application, so in the conclusions we will build on the development specifically on this platform.

    Closing our eyes to the well-known shortcomings of this popular paradigm, its most important advantage is the flexibility of its development. Why is this a minus? It is quite obvious that flexibility, in addition to the basic ability to solve business problems, makes it possible to do this in many different ways. It is true that there will be a dozen wrong ones for one correct approach, despite the fact that the business problem will be correctly solved in any of the cases, but with differences in implementation, the extensibility and transparency of which will depend on the correctness of the applied approach.

    Taking into account the law of Murphy, the conclusion suggests itself that, in the absence of proper architectural constraints, everything is more likely to follow the path of chaos, that is, the quality of the code will rapidly decrease, only because the paradigm allows it. This article will discuss one of the possible architectural constraints, which will help to keep the balance of good and evil, in other words, the dynamics of entropy growth in the project code base. It is important that this dynamic directly correlates with the number of people involved in writing the code, therefore, for large projects, the correctness of the selected constraints is especially critical. What is the point?

    The bottom line is simple. Let us push aside from the next axiom - the more properties in an object, the “worse” it is. This statement can be explained as follows: with an increase in the number of the internal state, all the positive indicators of the object — extensibility, modularity, transparency, testability — decrease. One may argue, saying that the complexity of an object and its functional concepts are interrelated and that there is no second without the first. That's right, but, following the “stateful” path, the growth of complexity occurs exponentially, while ideally the growth should be linear, or, to put it simply, the new feature should not complicate existing features.

    PS Here it is worth clarifying that features are understood as semantically related layers of business logic, so it is often decided to complicate the existing class rather than create a new one. It is quite difficult to find in such cases a contradiction with the first principle of SOLID.

    As an example, a completely standard screen with a list of entities with a choice is given. Some of the properties could have been made non-mutable, but IB binds us to the default controller constructor and this forces us to “make dirt”. Access to these properties is implicitly obtained by each class method within the same file. And the most unpleasant moment is that their mutability is also not covered by anything, and this somehow leads to irreversible consequences in the form of strong connectivity and, consequently, to the fact that the fix of some defects causes the appearance of new ones:

    It can be concluded that a linear increase in the complexity of an object with a general state is practically unattainable. The functional with this approach becomes monolithic and difficult to any segregation. A good example is the Massive-View-Controller anti-pattern, which is quite common and vividly demonstrates the result of the absence of any restrictions on a project.

    Using the example of UIKit, you can see how exactly the imperative style of development affects the complexity of the code and at what points the framework “forces” to start properties in classes.
    The simplest case is the processing of pressing a button — it is standardly performed only by defining the “dirty method”, that is, a function that cannot accept anything useful to the input, so you will have to refer to the data for data:

    Thus, the “feature” of the button will tyrannically complicate the rest of the functionality of the object, since all class members will have access to this data. In fact, almost any UI control in iOS works in a similar way. Therefore, the realization of some kind of wrapper over UI elements, for example, receiving a closure, comes to mind as a processing of the “triggering” of a particular element, but the difficulty is to figure out how to make such a wrapper concise and reliable. After all, it’s not only input, but also information output, for example, the ubiquitous data table also works “dirty” and has no idea about the data it shows, so you have to save it to the object:

    Conveniently? Conveniently, everyone always has access to the data. Flexible, fast, imperative. But this all turns over time, as a rule, into a concrete obelisk for a thousand lines of code and into tears of those who got the task to work in this class.

    Returning to the limitations of the above, the following principle can be formed: code without properties in classes is much cleaner, and more advantageous with support and extension. Ideally, the logic of an object should consist in its “pure” methods, which accept all their dependencies as input, and have the result of their activity at output.
    The idea is to lower the private state of the object to a lower level. That is, if private closes properties from the outside world, then our task is to go further and strengthen the encapsulation to the level of the methods themselves. At first glance, with all the asynchronous nature of the platform and the imperativeness of the main framework, it may seem that this whole undertaking is not that impossible, but at least its implementation will be felt unnatural. And, most likely it will be so, but this is one of those problems that can be solved by introducing an additional level of abstractions.

    If we want to fit all the logic of classes in their methods, without resorting to properties, we need to work closely with closures. For managing asynchronous operations, iOS has standard tools, such as GCD and OperationQueue. It seems that they would be enough, but when you try to bring the idea to life, everything will be far from being as rosy as you would like. With this approach, apart from the fact that there is a great chance to get a callback-hell, the code itself will be cumbersome, there will be many logical holes in it and it will be strongly connected. It is possible that such a code will be even more complicated than that from which we are trying so rapidly to escape.

    If you look around, you can see that there are much more beautiful and functional ways to achieve this goal. Reactive programming has been used for a long time in commercial software development and is ideal for the asynchronous frontend world. In this case, one (rather successful) implementation of the reactive paradigm for Swift - Rx will be considered.

    It offers a simple entity called Observable. This is a kind of abstraction over the flow of events, it can be subscribed to, and after that the subscriber will receive these events over time:

    It is easiest to imagine the flow of events by presenting a regular button. The event of its triggering is a thread here, therefore any object can subscribe and receive events of its pressing, and most importantly, the button does not know anything about its own subscribers. Conveniently, almost any action can be turned into a similar sequence of values, and this is important, because Observable can be combined with each other, in a way that no standard framework can do.

    For example, you can click a button (filtering a double tap) to send several requests, wait for the response of each of them, then combine the answer with what the user entered on the screen, execute another request and go to the next screen, passing the result to it, this Rx will provide an opportunity to succinctly handle errors and complete this chain (cancel requests) when leaving the screen, and all this logic will take two dozen lines of typed code: You should

    tell about the only property disposeBag. Apparently from screenshots, each subscription is located in it, and it is necessary for management of time of their life. That is, subscriptions are valid while “bag” lives in which they are placed, in this case, while the controller lives.

    In addition to compactness, it is difficult to make a mistake in the code above, since each closure returns something and does not contain side effects. This is the power we were looking for.

    One more important point can be noted: since the class has no properties, it is not necessary to write [weak self], which has a positive effect on the readability of the code. All functions can be better defined locally in the method where they are used, or carried out in separate classes. By the way, references to dependencies (ViewModel, Presenter, etc.) in this case can be passed as an argument to the controller method, in this case there is no need to save them in properties. It is right.

    After reviewing, it is time to apply Observable for bright purposes to simplify development. How exactly? Let us return to the idea of ​​“clean” methods and try to implement the logic of a small screen in its only method, for clarity, choose the method for terminating the view loading (viewDidLoad). Naturally, if the screen is set up in IB, then we will have to create properties for outlets, but this is not terrible, because the elements themselves do not constitute business logic, therefore they will not greatly affect the complexity of the screen. But if the screen is made up of code, then you can do without properties at all (except for disposeBag), creating elements in our method and immediately using them. How to deal with the imperativeness of UIKit elements which was described earlier? Rx, besides the approach itself, provides reactive wrappers for standard UI components, therefore, in most cases it is possible to obtain the necessary sequence of events on the spot. Or, on the contrary, to bind the existing Observable to, for example, a table — bring a request to it so that it updates its content immediately after its completion:

    Binding to collections is quite flexible, but by default, it works only through reloadData. For point updates there is a great debugged library from the same author - RxDataSources. With it, you can forget about crashes during batchUpdates.

    What happens next? The unified screen method will grow and in a rather complicated case it will become difficult to maintain. And when this happens, it will suddenly become clear that this method does not depend on anything other than itself, and the reactive approach has divided the code into logical blocks, which can be easily transferred into separate objects, designing them in a similar way. But this time, the methods will already take some dependencies on the screen and return some result back. The positive point is that the signature in this case is the most meaningful, it is clear that the function requires for its work and what its result is. It may look something like this:

    Separate structures help to keep the method cap readable and tidy, since there can be many dependencies.

    It is important to understand that the method does not necessarily have to be the only one in the whole object, the essence is in their independence from each other. Thanks to Rx, their input and output can be asynchronous and represent one or more Observables, providing another dimension for manipulating data.

    This approach unleashes hands and allows you to implement screens of almost any complexity, while keeping their code explicit and loosely coupled.

    Also popular now: