Typical OOP Misconceptions

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.



All Articles