Hello, Habr!
Today, a translated publication awaits you, to some extent reflecting our searches related to new books on OOP and FI. Please participate in the voting.
Is the OOP paradigm dead? Is it possible to say that functional programming is the future? It seems that many articles write about this. I tend to disagree with this point of view. Let's discuss!
Every few months I come across a post on some blog where the author makes seemingly well-grounded claims to object-oriented programming, after which she declares OOP a relic of the past, and we all have to switch to functional programming.
Earlier, I
wrote that OOP and FI do not contradict each other. Moreover, I managed to combine them very successfully.
Why do the authors of these articles have so many problems with OOP, and why does AF seem to them such an obvious alternative?
How to teach OOP
When we are taught OOP, they usually emphasize that it is based on four principles:
encapsulation ,
inheritance ,
abstraction ,
polymorphism . It is these four principles that are usually criticized in articles where the authors reason about the decline of the PLO.
However, OOP, like FI, is a tool. To solve problems. It can be consumed, it can also be abused. For example, by creating the wrong abstraction, you abuse OOP.
So, the
Square
class should never inherit the
Rectangle
class. In a mathematical sense, they are, of course, connected. However, from a programming point of view, they are not in an inheritance relationship. The fact is that the requirements for a square are stricter than for a rectangle. Whereas in a rectangle there are two pairs of equal sides, a square must have all sides equal.
Inheritance
Let's discuss inheritance in more detail. You probably recall textbook examples with beautiful hierarchies of inherited classes, and all these structures work to solve the problem. However, in practice, inheritance is not used as often as composition.
Consider an example. Let's say we have a very simple class, a controller in a web application. Most modern frameworks assume that you will work with it like this:
class BlogController extends FrameworkAbstractController { }
It is assumed that this way it will be easier for you to make calls like
this.renderTemplate(...)
, since such methods are inherited from the
FrameworkAbstractController
class.
As indicated in many articles on this subject, a number of tangible problems arise here. Any internal function in the base class actually turns into an API. She can no longer change. Any protected variables of the base controller will now more or less relate to the API.
There is nothing to get confused about. And if we chose the approach with composition and dependency injection, it would have turned out like this:
class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } }
You see, you no longer depend on some foggy
FrameworkAbstractController
, but depend on a very well-defined and narrow thing,
TemplateRenderer
. In fact,
BlogController
does not inherit from any other controller, since it does not inherit any behavior.
Encapsulation
The second often criticized feature of OOP is encapsulation. In literary language, the meaning of encapsulation is formulated as follows: data and functionality are delivered together, and the internal state of the class is hidden from the outside world.
This opportunity, again, allows for use and abuse. The main example of abuse in this case is a leaky state.
Relatively speaking, suppose that the
List<>
class contains a list of elements, and this list can be changed. Let's create a class to process an order basket as follows:
class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } }
Here, in most OOP-oriented languages, the following will happen: the items variable will be returned by reference. Therefore, further we can do this:
shoppingCart.getItems().clear();
Thus, we will actually clear the list of items in the basket, and ShoppingCart will not even know about it. However, if you look closely at this example, it becomes clear that the problem is not in the principle of encapsulation. This principle is just violated here, because the internal state leaks from the
ShoppingCart
class.
In this particular example, the author of the
ShoppingCart
class could use
immutability to get around the problem and make sure that the encapsulation principle is not violated.
Inexperienced programmers often violate the principle of encapsulation in another way: they introduce a state where it is not needed. Such inexperienced programmers often use private class variables to transfer data from one function to another within the same class, while it would be more appropriate to use Data Transfer Objects to transfer a complex structure to another function. As a result of such errors, the code is excessively complicated, which can lead to bugs.
In general, it would be nice to dispense with the state altogether - store mutable data in classes whenever possible. By doing so, you need to
ensure reliable encapsulation and make sure that there are no leaks anywhere.
Abstraction
Abstraction, again, is understood in many ways incorrectly. In no case should you stuff the code with abstract classes and make deep hierarchies in it.
If you do this without good reason, then you are just looking for trouble on your own head. It doesnβt matter how the abstraction is done - as an abstract class or as an interface; in any case, extra complexity will appear in the code. This complexity must be justified.
Simply put, an interface can only be created if you are willing to spend time and document the behavior that is expected from the class that implements it. Yes, you read me right. Itβs not enough just to make a list of the functions that you need to implement - also describe how (ideally) they should work.
Polymorphism
Finally, let's talk about polymorphism. He suggests that one class can implement many behaviors. A bad textbook example is to write that
Square
with polymorphism can be either a
Rectangle
or a
Parallelogram
. As I have already pointed out above, this in the OOP is decidedly impossible, since the behavior of these entities is different.
Speaking of polymorphism, one should keep in mind
behaviors , not
code . A good example is the
Soldier
class in a computer game. It can implement both
Movable
behavior (situation: it can move) and
Enemy
behavior (situation: shoots you). In contrast, the
GunEmplacement
class can only implement
Enemy
behavior.
So, if you write
Square implements Rectangle, Parallelogram
, this statement does not become true. Your abstractions should work according to business logic. You should think more about behavior than about code.
Why FP is not a silver bullet
So, when we repeated the four basic principles of OOP, let's think about what is the feature of functional programming, and why not using it to solve all the problems in your code?
From the point of view of many adherents of FP, classes are
sacrilege , and the code should be presented in the form of
functions . Depending on the language, data can be transferred from function to function using primitive types, or in the form of one or another structured set of data (arrays, dictionaries, etc.).
In addition, most functions should not have side effects. In other words, they should not change data in any unexpected place in the background, but only work with input parameters and produce output.
This approach separates the
data from the
functional - at first glance, this FP radically differs from OOP. FP emphasizes that in this way the code remains simple. You want to do something, write a function for this purpose - thatβs all.
Problems begin when some functions must rely on others. When function A calls function B, and function B calls another five to six functions, and at the very end
a zero-filling function is found that can break - this is where you will not be envied.
Most programmers who consider themselves proponents of FP love FP for its simplicity and do not consider such problems to be serious. This is fairly honest if your task is to simply pass the code and never think about it again. If you want to build a code base that is convenient in support, it is better to adhere to the principles of
pure code , in particular, apply
dependency inversion , in which the FI in practice also becomes much more complicated.
OOP or FP?
OOP and FI are
tools . Ultimately, it doesn't matter which programming paradigm you use. The problems described in most articles on this topic relate to code organization.
In my opinion, the macrostructure of the application is much more important. What are the modules in it? How do they exchange information with each other? What data structures are most common with you? How are they documented? Which objects are most important in terms of business logic?
All these issues are in no way connected with the programming paradigm used; at the level of such a paradigm they cannot even be solved. A good programmer studies the paradigm in order to master the tools it offers, and then chooses which ones are best suited to solve the task.