OOP in pictures

OOP (Object Oriented Programming) has become an integral part of the development of many modern projects, but despite its popularity, this paradigm is far from the only one. If you already know how to work with other paradigms and would like to familiarize yourself with the OOP occultism, then ahead of you is a little longrid and two megabytes of pictures and animations. Transformers will serve as examples.







The first thing to answer is why? Object-oriented ideology was developed as an attempt to connect the behavior of an entity with its data and project real-world objects and business processes into program code. It was thought that such a code is easier to read and understand by a person, since it is common for people to perceive the surrounding world as a multitude of objects interacting with each other, amenable to a certain classification. Is it possible for ideologists to achieve the goal, it’s difficult to answer unequivocally, but de facto we have a lot of projects in which the programmer will require OOP.



You should not think that OOP will somehow miraculously speed up the writing of programs, and expect a situation where Villaribo residents have already rolled out the OOP project to work, and Villabaggio residents are still washing the fat spaghetti code. In most cases, this is not so, and time is saved not at the development stage, but at the support stages (expansion, modification, debugging and testing), that is, in the long term. If you need to write a one-time script that does not need subsequent support, then OOP in this task is most likely not useful. However, a significant part of the life cycle of most modern projects is precisely support and expansion. The mere existence of OOP does not make your architecture flawless, and on the contrary can lead to unnecessary complications.



Sometimes you may come across criticism about the performance of OOP programs. True, a slight overhead is present, but so insignificant that in most cases it can be neglected in favor of advantages. Nevertheless, in bottlenecks where millions of objects per second are to be created or processed in one thread, it is worth at least revising the need for OOP, because even minimal overhead in such quantities can significantly affect performance. Profiling will help you capture the difference and make a decision. In other cases, say, where the lion's share of speed rests on IO, abandonment of objects will be a premature optimization.



By its very nature, object-oriented programming is best explained with examples. As promised, our patients will be transformers. I am not a transformer, and I have not read comics, therefore I will be guided by wikipedia and fantasy in the examples.



Classes and Objects



Immediately lyrical digression: an object-oriented approach is possible without classes, but we will consider, I apologize for the pun, the classical scheme, where classes are our everything.



The simplest explanation: a class is a drawing of a transformer, and instances of this class are specific transformers, for example, Optimus Prime or Oleg. And although they are assembled according to one drawing, they can walk, transform and shoot the same way, they both have their own unique state. A state is a series of changing properties. Therefore, in two different objects of the same class, we can observe a different name, age, location, charge level, amount of ammunition, etc. The very existence of these properties and their types are described in the class.



Thus, a class is a description of what properties and behavior an object will possess. And an object is an instance with its own state of these properties.



We say “properties and behavior”, but it sounds somehow abstract and incomprehensible. It will be more familiar for a programmer to sound like this: “variables and functions”. In fact, “properties” are the same ordinary variables, they are simply attributes of some object (they are called object fields). Similarly, “behavior” is an object’s functions (they are called methods), which are also attributes of an object. The difference between the method of the object and the usual function is only that the method has access to its own state through the fields.



In total, we have methods and properties that are attributes. How to work with attributes? In most PLs, the attribute reference operator is the point (except for PHP and Perl). It looks something like this (pseudo-code):



//       class class Transformer(){ //   x int x //    (     0) function constructor(int x){ //   x // (  0    ) this.x = x } //   run function run(){ //      this this.x += 1 } } //    : //        0 optimus = new Transformer(0) optimus.run() //    print optimus.x //  1 optimus.run() //      print optimus.x //  2
      
      





In the pictures I will use the following notation:







I did not use UML diagrams, considering them insufficiently visual, although more flexible.





Animation number 1



What do we see from the code?



1. this is a special local variable (inside methods) that allows an object to access its own attributes from its methods. I draw your attention that only to your own, that is, when the transformer calls its own method, or changes its own state. If the call looks like this outside: optimus.x , then from the inside, if Optimus wants to refer to his field x himself, in his method the call will look like this.x , that is, " I (Optimus) refer to my attribute x ". In most languages, this variable is called this, but there are exceptions (for example, self)



2. constructor is a special method that is automatically called when an object is created. The constructor can accept any arguments, like any other method. In each language, the constructor is indicated by its name. Somewhere these are specially reserved names like __construct or __init__, and somewhere the name of the constructor must match the class name. The purpose of the constructors is to initialize the object, fill in the required fields.



3. new is a keyword that must be used to create a new instance of a class. At this point, an object is created and the constructor is called. In our example, 0 is passed to the constructor as the starting position of the transformer (this is the aforementioned initialization). The new keyword is missing in some languages, and the constructor is called automatically when you try to call the class as a function, for example: Transformer ().



4. The constructor and run methods work with the internal state, but in all other respects do not differ from ordinary functions . Even the syntax of the declaration matches.



5. Classes may possess methods that do not need state and, as a consequence, create an object. In this case, the method is made static .



Srp



(Single Responsibility Principle / First SOLID Principle). You are probably already familiar with it from other paradigms: “one function should perform only one completed action”. This principle is also valid for classes: "One class must be responsible for any one task." Unfortunately with classes, it’s more difficult to define the line that needs to be crossed in order for the principle to be violated.



There are attempts to formalize this principle by describing the purpose of a class with one sentence without unions, but this is a very controversial technique, so trust your intuition and do not rush to extremes. You don’t need to make a Swiss knife from a class, but to produce a million classes with one method inside is also stupid.



Association



Traditionally, in the fields of an object not only ordinary variables of standard types can be stored, but also other objects. And these objects can in turn store some other objects and so on, forming a tree (sometimes a graph) of objects. This relationship is called association.



Suppose our transformer is equipped with a gun. Although no, better with two guns. In each hand. The guns are the same (they belong to the same class, or, if you wish, made according to one drawing), both can shoot and reload equally, but each has its own ammunition storage (own state). How now to describe it in OOP? By association:



 class Gun(){ //    int ammo_count //    function constructor(){ //  this.reload() //    "" } function fire(){ //    "" this.ammo_count -= 1 //      } function reload(){ //   "" this.ammo_count = 10 //     } } class Transformer(){ //    Gun gun_left //   " "   Gun gun_right //   " "    /*            ,    */ function constructor(Gun gun_left, Gun gun_right){ this.gun_left = gun_left //      this.gun_right = gun_right //      } //    "",   ... function fire(){ //  ,    "" this.gun_left.fire() //    ,     "" this.gun_right.fire() } } gun1 = new Gun() //    gun2 = new Gun() //    optimus = new Transformer(gun1, gun2) //  ,    
      
      







Animation number 2



this.gun_left.fire () and this.gun_right.fire () are calls to child objects that also happen through dots. At the first point, we turn to the attribute of ourselves (this.gun_right), getting the gun object, and at the second point, we turn to the method of the gun object (this.gun_right.fire ()).



Bottom line: the robot was made, the service weapon was issued, now we will understand what is happening here. In this code, one object has become an integral part of another object. This is the association. It, in turn, is of two types:



1. Composition - the case when, at the transformer factory, collecting Optimus, both guns are tightly pinned to his hands with nails, and after Optimus's death, the guns die with him. In other words, the life cycle of the child is the same as the life cycle of the parent.



2. Aggregation - the case when a gun is issued as a gun in his hand, and after Optimus’s death, this gun can be picked up by his comrade Oleg, and then taken into his hand, or turned into a pawnshop. That is, the life cycle of a child object does not depend on the life cycle of the parent, and can be used by other objects.



The orthodox OOP church preaches us a fundamental trinity - encapsulation, polymorphism and inheritance , on which the whole object-oriented approach is based. Let's sort them in order.







Inheritance



Inheritance is a system mechanism that allows, however paradoxical it may sound, to inherit properties and behavior of other classes by some classes for further expansion or modification.



What if, we do not want to stamp the same transformers, but want to make a common frame, but with a different body kit? OOP allows us such a prank by dividing the logic into similarities and differences, followed by the removal of the similarities in the parent class, and the differences in descendant classes. What does it look like?



Optimus Prime and Megatron are both transformers, but one is an Autobot and the other is a Decepticon. Suppose that the differences between Autobots and Decepticons will only consist in the fact that Autobots are transformed into cars, and Decepticons - into aviation. All other properties and behavior will not make any difference. In this case, the inheritance system can be designed as follows: common features (running, shooting) will be described in the Transformer base class, and differences (transformation) in the two child classes Autobot and Decepticon.



 class Transformer(){ //   function run(){ // ,    } function fire(){ // ,    } } class Autobot(Transformer){ //  ,   Transformer function transform(){ // ,      } } class Decepticon(Transformer){ //  ,   Transformer function transform(){ // ,      } } optimus = new Autobot() megatron = new Decepticon()
      
      







Animation number 3



This example illustrates how inheritance becomes one of the ways to deduplicate code ( DRY principle ) using the parent class, and at the same time provides opportunities for mutation in descendant classes.



Overload



If you override the existing method in the parent class in the parent class, the overload will work. This allows us not to supplement the behavior of the parent class, but to modify it. At the time of calling the method or accessing the field of the object, the search for the attribute occurs from the descendant to the very root - the parent. That is, if the fire () method is called on the autobot, the method is first searched for in the descendant class - Autobot, and since it is not there, the search rises one step higher - to the Transformer class, where it will be detected and called.



Inappropriate use



It is curious that an excessively deep hierarchy of inheritance can lead to the opposite effect - complication when trying to figure out who is inherited from whom and which method is called in which case. In addition, not all architectural requirements can be implemented using inheritance. Therefore, inheritance should be applied without fanaticism. There are recommendations calling for a preferred composition over inheritance, where appropriate. Any criticism of inheritance that I have met is reinforced by unsuccessful examples when inheritance is used as a golden hammer . But this does not mean at all that inheritance is always harmful in principle. My narcologist said that the first step is to admit that you are dependent on inheritance.



When describing the relations of two entities, when is inheritance appropriate and when is composition appropriate? You can use the popular cheat sheet: ask yourself, is entity A the essence of B ? If so, then most likely inheritance is suitable. If entity A is part of entity B , then our choice is composition.



In relation to our situation, it will sound like this:



  1. Is Autobot Transformer? Yes, then we choose inheritance.
  2. Is the gun part of the Transformer? Yes, that means composition.


For self-testing, try the reverse combination, you get garbage. This cheat sheet helps in most cases, but there are other factors that you should rely on when choosing between composition and inheritance. In addition, these methods can be combined to solve different types of problems.



Inheritance is Static



Another important difference between inheritance and composition is that inheritance is static in nature and establishes class relationships only at the interpretation / compilation stage. Composition, as we saw in the examples, allows you to change the relationship of entities on the fly right in runtime - sometimes this is very important, so you need to remember this when choosing relationships (unless of course there is a desire to use metaprogramming ).



Multiple inheritance



We examined a situation where two classes are inherited from a common descendant. But in some languages, you can do the opposite - inherit one class from two or more parents, combining their properties and behavior. The ability to inherit from multiple classes instead of one is multiple inheritance.







In general, there is an opinion in Illuminati circles that multiple inheritance is a sin, it carries with it a diamond - shaped problem and confusion with designers. In addition, tasks that can be solved by multiple inheritance can be solved by other mechanisms, for example, the interface mechanism (which we will also talk about). But in fairness, it should be noted that multiple inheritance is convenient to use for the implementation of impurities .



Abstract classes



In addition to ordinary classes, abstract languages ​​exist in some languages. They differ from ordinary classes in that you cannot create an object of such a class. Why do we need such a class, the reader will ask? It is needed so that descendants can be inherited from it - ordinary classes whose objects can already be created.



The abstract class, along with the usual methods, contains abstract methods without implementation (with a signature, but without code), which the programmer who plans to create a descendant class must implement. Abstract classes are not required, but they help to establish a contract that requires the implementation of a specific set of methods in order to protect a programmer with poor memory from an implementation error.



Polymorphism



Polymorphism is a system property that allows you to have many implementations of one interface. Nothing is clear. Let's turn to transformers.



Suppose we have three transformers: Optimus, Megatron and Oleg. Transformers are combat, so they have the attack () method. The player, pushing the “fight” button on his joystick, tells the game to call the attack () method on the transformer the player is playing for. But since the transformers are different, and the game is interesting, each of them will attack in some way. Let's say Optimus is an object of the Autobot class, and Autobots are equipped with cannons with plutonium warheads (yes, fans of transformers are not angry). Megatron is a Decepticon, and shoots from a plasma gun. Oleg is a bass player, and he calls him names. And what is the use?



The use of polymorphism in this example is that the game code does not know anything about the implementation of its request, who should attack how, its task is simply to call the attack () method, whose signature is the same for all character classes. This allows you to add new character classes, or change existing methods without changing the game code. It's comfortable.



Encapsulation



Encapsulation is the control of access to the fields and methods of an object. Access control implies not only possible / inconsequential, but also various validations, loadings, calculations, and other dynamic behavior.



In many languages, data hiding is part of encapsulation. For this, there are access modifiers (we will describe those that are in almost all OOP languages):





 class Transformer(){ public function constructor(){ } protected function setup(){ } private function dance(){ } }
      
      





How to choose the access modifier? In the simplest case like this: if the method should be accessible to external code, select public. Otherwise, private. If there is inheritance, then protected may be required if the method should not be called externally, but should be called by descendants.



Accessors (getters and setters)



Getters and setters are methods whose task is to control access to fields. The getter reads and returns the value of the field, and the setter, on the contrary, takes the value as an argument and writes it to the field. This makes it possible to provide such methods with additional treatments. For example, a setter, when writing a value to an object field, can check the type or whether the value is in the range of valid values ​​(validation). In the getter, you can add lazy initialization or caching if the actual value actually lies in the database. There are many applications.



Some languages ​​have syntactic sugar that allows such accessors to be masked as properties, which makes access transparent to external code, which does not suspect that it works not with a field, but with a method that executes an SQL query or reading from a file under the hood. This is how abstraction and transparency are achieved.



Interfaces



The task of the interface is to reduce the level of dependence of entities on each other, adding more abstraction.



Not all languages ​​have this mechanism, but in OOP languages ​​with static typing without them it would be really bad. Above, we examined abstract classes, touching upon the topic of contracts, which are obliged to implement some abstract methods. So the interface looks very much like an abstract class, but it is not a class, but just a dummy with an enumeration of abstract methods (without implementation). In other words, the interface is declarative in nature, that is, a clean contract without a bit of code.



Typically, languages ​​that have interfaces do not have multiple class inheritance, but there is multiple interface inheritance. This allows the class to list the interfaces that it is committed to implement.



Classes with interfaces consist of a many-to-many relationship: a single class can implement multiple interfaces, and each interface, in turn, can be implemented by many classes.



The interface has two-sided use:



  1. On one side of the interface are classes implementing this interface.
  2. On the other side are consumers who use this interface as a description of the type of data with which they (consumers) work.


For example, if an object other than the basic behavior can be serialized, then let it implement the Serializable interface. And if the object can be cloned, then let it implement another interface - “Cloned”. And if we have some kind of transport module that transmits objects over the network, it will accept any objects implementing the Serializable interface.



Imagine that the transformer frame is equipped with three slots: a slot for weapons, for an energy generator and for some kind of scanner. These slots have certain interfaces: only suitable equipment can be installed in each slot. In the slot for weapons, you can install a rocket launcher or a laser gun, in the slot for the power generator - a nuclear reactor or RTG (radioisotope thermoelectric generator), and in the slot for the scanner - a radar or lidar. The bottom line is that each slot has a universal connection interface, and already specific devices must comply with this interface. For example, several types of slots are used on motherboards: a processor slot allows you to connect various processors suitable for this socket, and a SATA slot allows you to connect any SSD or HDD drive or even a CD / DVD.



I draw your attention to the fact that the resulting system of slots for transformers is an example of the use of composition. If the equipment in the slots will be replaceable during the life of the transformer, then this is already aggregation. For clarity, we will call the interfaces, as is customary in some languages, adding the capital “And” in front of the name: IWeapon, IEnergyGenerator, IScanner.



 //  : interface IWeapon{ function fire() {} //    .   } interface IEnergyGenerator{ //    ,     : function generate_energy() {} //  function load_fuel() {} //  } interface IScanner{ function scan() {} } // ,  : class RocketLauncher() : IWeapon { function fire(){ //    } } class LaserGun() : IWeapon { function fire(){ //    } } class NuclearReactor() : IEnergyGenerator { function generate_energy(){ //      } function load_fuel(){ //     } } class RITEG() : IEnergyGenerator { function generate_energy(){ //     } function load_fuel(){ //   - } } class Radar() : IScanner { function scan(){ //    } } class Lidar() : IScanner { function scan(){ //     } } //  - : class Transformer() { // , : IWeapon slot_weapon //      . IEnergyGenerator slot_energy_generator //     , IScanner slot_scanner //     /*         ,      ,   : */ function install_weapon(IWeapon weapon){ this.slot_weapon = weapon } function install_energy_generator(IEnergyGenerator energy_generator){ this.slot_energy_generator = energy_generator } function install_scanner(IScanner scanner){ this.slot_scanner = scanner } } //   class TransformerFactory(){ function build_some_transformer() { transformer = new Transformer() laser_gun = new LaserGun() nuclear_reactor = new NuclearReactor() radar = new Radar() transformer.install_weapon(laser_gun) transformer.install_energy_generator(nuclear_reactor) transformer.install_scanner(radar) return transformer } } //  transformer_factory = new TransformerFactory() oleg = transformer_factory.build_some_transformer()
      
      







Animation No. 4



Unfortunately, the factory did not fit into the picture, but it is still optional, the transformer can also be assembled in the yard.



The abstraction layer indicated in the picture in the form of interfaces between the implementation layer and the consumer layer makes it possible to abstract one from the other. You can observe this by looking at each layer separately: in the implementation layer (on the left) there is not a word about the Transformer class, and in the consumer layer (on the right) there is not a word about specific implementations (there are no words Radar, RocketLauncher, NuclearReactor, etc. . d.)



In this code, we can create new components for transformers without affecting the drawings of the transformers themselves. At the same time and vice versa, we can create new transformers by combining existing components, or add new components without changing existing ones.



Duck typing



The phenomenon that we observe in the resulting architecture is called duck typing : if something quacks like a duck, swims like a duck, and looks like a duck, then most likely it is a duck .



Translating this into the language of transformers, it will sound like this: if something shoots like a cannon, and reloads like a cannon, most likely it's a cannon. If the device generates energy, it is most likely a power generator.



Unlike the hierarchical typification of inheritance, with duck typing, the transformer does not care what class the gun was given to him, and whether it is a gun at all. The main thing is that this thing can shoot! This is not a virtue of duck typing, but rather a compromise. There may be a reverse situation, as in this picture below:







ISP

(Interface Segregation Principle / Fourth SOLID Principle) encourages not to create bold, universal interfaces. Instead, interfaces should be divided into smaller and specialized ones, this will help to combine them more flexibly in implementing classes, without forcing to implement unnecessary methods.



Abstraction



In OOP, everything revolves around abstraction. There are fanatics who claim that abstraction should be part of the OOP trinity (encapsulation, polymorphism, inheritance). And my parole inspector said the opposite: abstraction is inherent in any programming, and not just OOP, so it should be separate. On the other hand, the same can be said about the rest of the principles, but you won’t erase words from a song. One way or another, abstraction is needed, and especially in OOP.



Level of abstraction



Here one cannot fail to quote one well-known joke:

- any architectural problem can be solved by adding an additional layer of abstraction, except for the problem of a large number of abstractions.



In our example with interfaces, we introduced an abstraction layer between transformers and components, making the architecture more flexible. But at what cost?We had to complicate the architecture. My psychotherapist said that the ability to balance between the simplicity of architecture and the flexibility of the application is an art. When choosing a middle ground, one should rely not only on one’s own experience and intuition, but also on the context of the current project. Since the person has not yet learned to see the future, it is necessary to analytically estimate what level of abstraction and with what degree of probability can be useful in this project, how much time will be required to develop a flexible architecture, and whether the time spent will be paid off in the future.



Incorrect selection of the level of abstraction leads to one of two problems:



  1. , , , ( )
  2. , , , , . ( )






It is also important to understand that the level of abstraction is determined not for the entire project as a whole, but separately for different components. In some places, the abstraction system may not be enough, but in some places on the contrary - bust. However, the wrong choice of the level of abstraction can be corrected by timely refactoring. The keyword is timely . Delayed refactoring is problematic when many mechanisms are already implemented at this level of abstraction. Carrying out a refactoring ritual in running systems can involve acute pain in hard-to-reach places of a programmer. It's about how to change the foundation in a house - it’s cheaper to build a house next door from scratch.



Let's look at the definition of the level of abstraction from the possible options on the example of a hypothetical game "transformers-online." In this case, the abstraction levels will act as layers, each subsequent layer under consideration will lie on top of the previous one, taking part of the functional from it into itself.



First layer. The game has one class of transformer, all properties and behavior are described in it. This is a completely wooden level of abstraction, suitable for casual games, which does not imply any special flexibility.



Second level.The game has a basic transformer with basic abilities and classes of transformers with their own specialization (such as reconnaissance aircraft, attack aircraft, support), which is described by additional methods. Thus, the player is given the opportunity to choose, and the developers are easier to add new classes.



Third level. In addition to the classification of transformers, aggregation is introduced using a system of slots and components (as in our example with reactors, guns and radars). Now part of the behavior will be determined by what staf the player has installed in his transformer. This gives the player even more opportunities for customizing the game’s mechanics of the character, and gives developers the opportunity to add these same expansion modules, which in turn simplifies the work of game designers to release new content.



Fourth level. You can also include your own aggregation in the components, which allows you to select the materials and parts from which these components are assembled. This approach will give the player the opportunity not only to stuff the transformers with the necessary components, but also to independently produce these components from various parts. Frankly, I have never met such a level of abstraction in games, and not without reason! After all, this is accompanied by a significant complication of architecture, and adjusting the balance in such games turns into hell. But I do not exclude that such games exist.







As you can see, each described layer, in principle, has the right to life. It all depends on what kind of flexibility we want to lay in the project. If the terms of reference do not say anything about this, or the author of the project himself does not know what the business may require, you can look at similar projects in this area and focus on them.



Design patterns







Decades of development have led to the formation of a list of the most commonly used architectural solutions, which over time have been classified by the community, and are called design patterns . That is why when I first read about patterns, I was surprised to find that it turns out that I already use many of them in practice, I just did not know that these solutions have a name.



Design patterns, like abstraction, are characteristic not only of OOP development, but also of other paradigms. In general, the topic of patterns is beyond the scope of this article, but I would like to warn a young developer who only intends to get acquainted with patterns. It is a trap! Now I will explain why.



The purpose of the patterns is to help solve architectural problems that are either already discovered, or most likely to be discovered during the development of the project. So, after reading about patterns, a beginner may have an irresistible temptation to use patterns not to solve problems, but to generate them. And since the developer is unbridled in his desires, he may not start to solve the problem with the help of patterns, but can adjust any tasks to the solutions with the help of patterns.



Another value from patterns is the formalization of terminology. It’s much easier for a colleague to say that a “chain of duties” is used in this place than to draw the behavior and relations of objects on paper for half an hour.



Conclusion



In modern conditions, the presence of the word class in your code does not make you an OOP programmer. For if you do not use the mechanisms described in the article (polymorphism, composition, inheritance, etc.), and instead use classes only to group functions and data, then this is not OOP. The same can be solved by some namespaces and data structures. Do not confuse, otherwise you will be ashamed at the interview.



I want to finish my song with important words. Any described mechanisms, principles and patterns, as well as OOP as a whole, should not be applied where it is pointless or could be harmful. This leads to articles with strange headlines like “Inheritance is the cause of premature aging” or “Singleton can lead to cancer.”



I'm serious.If we consider the case of singleton, then its widespread use without knowledge of the case, has caused serious architectural problems in many projects. And lovers of hammering nails with a microscope kindly called him an antipattern. Be prudent.



Unfortunately, in designing there are no unambiguous recipes for all occasions, where what is appropriate and where is inappropriate. This will gradually fit into the head with experience.



All Articles