Most OOP fans are also polymorphic fans. Many good books (take at least Fowler's "Refactoring") even go to extremes and say: if you use run-time type checks (such as the
instanceof
operation in Java), then you are most likely a terrible monster at heart. From those that scare small children with
switch
operators.
Generally speaking, I recognize that the use of
instanceof
and its analogs is
usually the result of insufficient OOP design skills. Polymorphism is better than type checks. It makes the code more flexible and understandable. However, there is at least one common case where you definitely cannot use polymorphism. Moreover, this case is so widespread that it can already be considered a pattern. I would love to use polymorphism in it, honestly. And if you know how to do it - tell me. But I do not think that this is possible. At least not exactly in static languages like Java or C ++.
Polymorphism definition
In case you are not familiar with the terminology of the PLO, I will explain what is being said. Polymorphism is a pretentious term for the concept of late binding. Late binding, in turn, is a pretentious designation (you will find a pattern here if you dig deeper) for a situation in which the decision on which particular method will be called is postponed until the program starts. Thus, the verification of the compliance of the object and the message (i.e. method) will be performed during the course of the application operation.
')
In performance-oriented programming languages such as C ++, Java or OCaml, numbers are assigned to methods, and then a table of its methods is created for each class. Which is the search at run time. In languages that prefer flexibility and dynamism, the search is carried out not among the numbers, but among the hashed method names. For the rest, these two approaches almost coincide.
Virtual methods alone do not generate polymorphism. It comes into play only when a class has several subclasses. And each of which implements its own, special, version of the polymorphic method. In the banal example from the textbook, this would be illustrated by the analogy with the zoo, in which all animals handle the message differently. (Although, in fact, the textbooks lie - all smells are pretty damn similar, it’s just their
size . In my humble opinion, of course. True, I still haven’t decided who has this maximum
value - a hippo or giraffe? )
Polymorphism in action
As an example, let's take a look at the classical problem of calculating a mathematical expression that is often found at interviews. It was first used by Ron Braunstein in Amazon (as far as I know). The task is quite complex and allows you to check the possession of many important skills. These include OOP design, recursion, binary trees, paired polymorphism with dynamic typing, general programming skills, and even (if you suddenly want to complicate the task
to the maximum ) the theory of syntactic analysis.
So, considering this task, the candidate at some point realizes that if you use only binary operations, such as “+”, “-”, “*”, “/”, then the arithmetic expression can be represented as a binary tree. All leaves of a tree will be numbers, and all intermediate nodes will be operations. The expression will be calculated by traversing the tree. If the applicant cannot come to such a decision on his own, you can delicately hint. Or, if things are really bad, say in the forehead. After all, even after this, the task will still remain interesting.
The first half of it, which some people (whose names I will take with me to the grave, but whose initials is Willy Lewis) is considered a Necessary Requirement For Those Who Want to Call Myself a Developer And Work In Amazon, is actually quite complicated. The question here is how to move from a string with an arithmetic expression, such as “2 + (2)”, to an expression tree. And this is a serious question.
We are also interested in the second half of the problem: suppose that you solve it together and your partner is responsible for transforming the string into a tree (we will call him Willy). You just need to decide which classes Willie will build his tree from. You can choose any language. The main thing, do not forget to do it, and in fact Willie can give preference to the assembler. And the assembler has long been out of production processor. If you are in a bad mood, of course.
You will be amazed how many subjects this stage baffles.
I seem to have already let it slip about the correct answer, but somehow the rating of the solutions is as follows. The standard Bad Solution is to use a
switch
or
case
(at the very least - good old cascading
if
). A slightly Improved Solution will use a table of function pointers. And, finally, Perhaps the Best Solution will apply polymorphism. Try to implement each of them at your leisure. It delivers!
Ironically (as you will see later), a solution with polymorphism is ideal for an expandable system. If you want to add new features without having to recompile everything from and to, and in particular, without having to add more and more new cases to your Giant Operator Switch Composed of 500 Cases, then you just
have to use polymorphism.
Threefold polymorphic hurray in honor of polymorphism
So polymorphism, anyway, seems useful. Perhaps the most successful of its applications is the polymorphic print output operator print. If you are programming in Java, Python, Ruby, or any other "real" object-oriented language, you probably take it for granted. You ask the object to print itself and, by golly, it does. Each object reports about itself as much as you need to know about its internal state. This is very useful for debugging, tracing, logging, and perhaps even documentation.
If you use a crippled fake OOP language, such as C ++ or Perl, to which all object-orientation is bolted as a pair of disks for $ 2500 to the 1978 Subaru Legacy of release, then you are probably bogged down in the debugger. Or
Data::Dumper
'e. Or something else like that. In general, it sucks you!
(The rhetorical question: why do we choose C ++ or Perl? These are the two most terrible languages in the world! We could just as well use Pascal or Cobol, is it not clear?)
By the way, polymorphic
print
is the main reason why I haven’t been writing about OCaml lately. For reasons that I have not yet fully realized, but which are definitely in the list of the Most Insane Motives of Language Designers, OCaml does not have a polymorphic
print
. Therefore, you cannot output arbitrary objects to the console for debugging. I
try to believe that it took to achieve legendary, even exceeding C ++, performance. Because any other reason would be a monstrous insult to usability. Well, then, they have a debugger capable of returning the program back in time. It will definitely come in handy more than once.
So we all love polymorphism. This is an alternative to micromanagement. You ask objects to do something without telling
how to do it, and they obediently obey. Spending the day watching online Strong Bad clips. Oh, those stupid objects! It is impossible not to love them!
But polymorphism, like all worthy heroes, has the Dark Side. Of course, not as dark as Anakin Skywalker, but nonetheless.
Polymorphism paradox
The use of polymorphism suggests a rarely spoken out loud, but a very important condition: you should be able to change the code in the future. Indeed, at least in statically typed languages, such as Java and C ++, during the addition of the polymorphic method, it is required to recompile all the classes that implement this method. And this, in turn, means that you need to have access to their source code. And also the ability to modify it.
There is a certain class of systems for which this is impracticable - the so-called extensible systems.
Suppose you are designing a hypothetical system that allows users to add their own code. This is not a trivial task for many reasons, including the need to protect against unauthorized access, ensure streaming security, and much, much more. But such systems exist! For example, there are online games that allow players to make changes without having access to the original source code. In fact, most of the multiplayer online games are moving in this direction - in the management of the companies they realized that users can and will create excellent content themselves. Therefore, the games open their API and allow players to expand the program, creating their own monsters, their spells and then on the list.
Something tells me that web services are in the same boat with online games.
Every time you create such an expandable system, you have to work three times more. Design internal APIs and classes so that they can be modified by end users.
A good example is Java Swing. Each expandable system faces the paradox of the inventor. You can read more about this paradox somewhere else, let me just say the essence: you cannot predict in advance what changes you would like to make to users. You can do anything — even put every line of code out as a separate virtual function — but users will inevitably run into something they want, but cannot modify. This is a real tragedy - there is no elegant solution. Swing tries to fight by giving lots of hooks. But this makes its API terribly cumbersome and difficult to master.
The essence of the problem
To make the conversation more specific, let's go back to the example of online games. Suppose you all perfectly designed and published the API and classes to create and manage spells, monsters and other game objects. Suppose you have a large base of monsters. I bet you can imagine this if you try.
Suppose now that one of the players wanted to create a small pet named Evaluative Elf. This, of course, is a far-fetched example, which works in the same way as proving the problem of stopping, but a similar situation is quite possible in real life.
Let the only meaning of life of our Estimated Elf be the announcement of whether he likes other monsters or not. He sits on your shoulder and whenever you meet, say, Ork, he shouts bloodlustly: “I hate orcs !!! Aaaaaaaa !!! "(By the way, I feel such feelings towards C ++)
The polymorphic solution to this problem is simple: go through each of your 150 monsters and add the
()
method to them.
Heck! It even sounds crazy stupid. But this is the true polymorphic approach, isn't it? If there is a group of similar objects (in our case, monsters), and they all have to respond to the same situation in a different way, then you add a virtual method to them and implement it differently for different objects. Right?
Obviously, this approach will not work in our case, and even if it
could work (and it cannot, because the user who wrote this little elf does not have access to the source codes), he would definitely have a taste of Bad Design. Of course, there is no reason to add such a specific method to each monster in the game. What if it later turns out that the Valuation Elf is infringing copyright and should be removed? You will have to return everything to its original state by removing this method from all 150 classes.
As far as I know (and I do not claim the laurels of a good designer, I just want to find the right answer), the correct solution is the dynamic type definition. The code will look something like this:
public boolean ( mon) { if (mon instanceof ) { return false; } if (mon instanceof ) { return true; } ... < 150 > }
Of course, you can turn out to be OOP freaks and create 150 auxiliary classes for the Estimated Elf, one for each monster. It still does not solve the root of the problem, because its essence lies in the fact that the distinguished behavior does not apply to the called party, but to the caller. It belongs to her.
In some high-level languages, the problem is solved a little more elegantly (I emphasize only a little). Ruby, for example, supports adding methods to other classes. And even the library. And even if you do not have the source codes. For example, you can put the following code in the Estimated Elf file:
class def ; return false; end end class def ; return false; end end class def ; return true; end end ...
Ruby will load all the listed classes, if they are not already loaded, and add your method to each of them. This is a very cool opportunity, generally speaking.
But this approach has both advantages and disadvantages. How does it work? In Ruby (as in most other high-level languages), methods are just entries in a hash table of the corresponding class. And then you appear and add your entry to the hash table of each of the Monster subclasses. Benefits:
- the entire code of the Estimated Elf is contained in its file;
- no code is added to classes until the elf file is uploaded;
- not only an elf, but anyone else in the system can ask the monster whether he likes an elf or not.
The disadvantage is that you have to provide a default behavior for the case when the elf does not recognize the monster, because he was added to the game after writing the elf. If someone comes up with a Gremlin, your elf will hang, shouting something like, “Damn it, what is it ?!” until you update his code by adding gremlins to it.
I think if it would be possible to sort through all the classes in the system and check whether they are descendants of the Monster, then everything would be solved with a few lines of code. In Ruby, I bet it is possible ... but only for already loaded classes. For classes still on disk, this will not work! Surely you can get around this problem, but besides the disk, there is also a network ...
However, the need for default behavior is not the worst. There are much more serious disadvantages. Let's say thread safety. It seriously bothers me - I do not think that the semantics of Ruby for thread safety in this case is clearly defined. Will there be synchronization at the class level? What happens to streams of pre-elf class instances? I still do not know enough Japanese to understand the specifications or source code.
But what
really is the problem, what
really annoys me, is that the code begins to multiply in all classes in the system. Smacks of encapsulation.
In fact, it is still worse. Smacks of Bad Design. We get a situation in which the observer makes a kind of judgments and we attach the code of these judgments to the objects of observation. It looks as if I walked around colleagues from my floor and handed each individual a badge with the words: “Please, don't put it anywhere. According to him, I understand whether you like me or not. " In the real world, everything works differently, and the PLO is supposed to model the real world.
Polymorphism revision
Well, now that I have formulated my idea
so clearly, polymorphism no longer seems like a silver bullet. Even in non-extensible systems, if you need to make some choice based on the type of object, it does not make sense to transfer the choice to this object itself.
As an example, we can take authentication for a more practical and practical way. Let me ask you: if you were to develop an access control system, would you do a virtual method have the
()
, forcing all stakeholders to implement this method? That is - would you put at the entrance of the guard, asking each incoming, whether access to the building is allowed?
No way! You would have to add to the runtime verification code:
public boolean ( s) { return (s.() || s.() || s.()); }
But wait, there is no direct class checking used anywhere. I did not write, for example,
s instanceof
. What is the matter?
Well, the “type” of an object is, in essence, a collection of its class (which is clearly fixed and unchanged) and its properties (which can be both fixed and changing at run time). This is a topic for a separate discussion, but it seems to me that the type is determined more by properties than classes. It is because of the innate inflexibility of the latter. But in "traditional" languages like C ++ and Java, this approach would make code reuse a bit more difficult due to the lack of syntactic support for delegation. (If it suddenly seemed to you that this does not make sense, everything is in order: I am finishing my third glass of wine on the way to the penultimate stage. So let's leave this topic for another note.)
At the same time, I hope that I managed to clearly express the main idea -
polymorphism only makes sense when the polymorphic behavior actually belongs to the object .
If this is the behavior of the subject, then dynamic type checking is preferable.Summarizing
So, I hope you learned something useful from today's note. About myself, I'm sure. For example, I learned that Google’s search engine is really smart enough to fix En and Kin Skywalker by asking “Did you mean: En and Kin Skywalker?”. Oh, and arrogant guys! Not that copyright belonged to them.I also learned that the ideal length of a blog post is exactly two glasses of wine. If you go further, then you start to rant almost incoherently. Yes, and the speed of dialing flies to hell.Good luck.
The original is When Polymorphism Fails. Steve Yegge. Stevey's Drunken Blog Rants