📜 ⬆️ ⬇️

The book "Elegant Objects. Java Edition »

image Hi, Habrozhiteli! This book seriously revises the essence and principles of object-oriented programming (OOP) and can be metaphorically called "OOP Lobachevsky." Yegor Bugaenko , a developer with 20 years of experience, critically analyzes the PLO’s dogmas and suggests looking at this paradigm in a completely new way. So, he brands static methods, getters, setters, mutable methods, considering that this is evil. For a novice programmer, this volume can become an enlightenment or shock, and for an experienced programmer, it is mandatory reading.

Fragment "Do not use static methods"


Ah, static methods ... One of my favorite topics. It took me several years to realize how important this problem is. Now I regret all the time I spent writing procedural, not object-oriented software. I was blind, but now I see. Static methods are as big, if not even more of a problem in OOP, than the presence of the NULL constant. Static methods in principle should not be in Java, and in other object-oriented languages, but, alas, they are there. We should not be aware of such things as the static keyword in Java, but, alas, forced .. I do not know who exactly brought them to Java, but they are pure evil .. Static methods, not the authors of this feature. I hope.

Let's see what static methods are and why we still create them. Let's say I need the functionality of loading a web page via HTTP requests. I create such a "class":

class WebPage { public static String read(String uri) { //  HTTP- //     UTF8- } } 

It is very convenient to use it:
')
 String html = WebPage.read("http://www.java.com"); 

The read () method belongs to the class of methods that I oppose. I suggest using the object instead (I also changed the name of the method in accordance with the recommendations from section 2.4):

 class WebPage { private final String uri; public String content() { //  HTTP- //     UTF8- } } 

Here's how to use it:

 String html = new WebPage("http://www.java.com") .content(); 

You can say that there is not much difference between them. Static methods work even faster, because we don’t need to create a new object every time we need to download a web page. Just call the static method, it will do the work, you will get the result and will continue to work .. There is no need to mess around with the objects and the garbage collector. In addition, we can group several static methods into a utility class and name it, say, WebUtils.

These methods will help to load web pages, get statistical information, determine response time, etc. There will be many methods in them, and they can be used simply and intuitively. In addition, how to use static methods is also intuitive. Everyone understands how they work. Just write WebPage.read (), and - you guessed it! - the page will be read. We gave the computer an instruction, and it performs it .. Simple and clear, right? And no!

Static methods in any context is an unmistakable indicator of a bad programmer who has no idea about OOP. There is not a single excuse for using static methods in any situation. Performance care is not considered. Static methods are mockery of the object-oriented paradigm. They exist in Java, Ruby, C ++, PHP and other languages. Unfortunately. We cannot throw them out, we cannot rewrite all open source libraries full of static methods, but we can stop using them in our code.

We must stop using static methods.

Now we will look at them from several different positions and discuss their practical shortcomings. I can generalize them in advance for you: static methods degrade software maintainability. This should not surprise you. It comes down to maintainability.

Object thinking versus computer


Initially, I called this subsection “Objective thinking versus procedural”, but then renamed it. “Procedural thinking” means almost the same thing, but the phrase “thinking like a computer” better describes the problem .. We inherited this way of thinking from early programming languages ​​such as Assembly, C, COBOL, Basic, Pascal, and many others. The basis of the paradigm is that the computer works for us, and we tell it what to do by giving it explicit instructions, for example:

  CMP AX, BX JNAE greater MOV CX, BX RET greater: MOV CX, AX RET 

This is an assembler "subroutine" for an Intel 8086 processor. It finds and returns the larger of two numbers. We put them in the registers AX and BX, respectively, and the result falls in the register CX. Here is exactly the same code in C:

 int max(int a, int b) { if (a > b) { return a; } return b; } 

“What's wrong with that?” You ask. Nothing .. Everything is in order with this code - it works as it should be .. This is how all computers work. They expect that we will give them instructions that they will execute one after another .. For many years we have written programs in this way. The advantage of this approach is that we stay close to the processor, directing its further movement. We are at the helm, and the computer follows our instructions. We tell the computer how to find the larger of the two numbers. We make decisions, he follows them. The flow of execution is always consistent, from the beginning of the script to its end.

This linear type of thinking is called thinking like a computer. The computer at some point will begin to execute instructions and at some point will finish to do it. When writing code in C, we are forced to think this way. Operators separated by semicolons go from top to bottom. Such style is inherited from the assembler.
Although higher level languages ​​than assembler have procedures, subroutines, and other abstraction mechanisms, they do not eliminate a coherent way of thinking. The program is still run from top to bottom. In this approach, there is nothing wrong with writing small programs, but on a larger scale it is difficult to think so.

Take a look at the same code, written in a functional Lisp programming language:

 (defun max (ab) (if (> ab) ab)) 

Can you tell where the execution of this code begins and ends? Not. We do not know how the processor will get the result, nor how the if function will work. We are very detached from the processor. We think as a function, not as a computer. When we need a new thing, we define it:

 (def x (max 5 9)) 

We define, but do not give instructions to the processor. With this line we bind x to (max 5 9). We do not ask the computer to calculate the larger of the two numbers. We simply say that x is the larger of the two numbers. We do not control how and when it will be calculated. Please note this is important: x is the larger of the numbers. The “is” relationship (“to be”, “to be”) is what distinguishes the functional, logical and object-oriented programming paradigm from the procedural one.

With a computer mindset, we are at the helm and control the flow of instructions. With an object-oriented way of thinking, we simply determine who is who, and let them interact when they need it. Here’s how the calculation of the larger of two numbers should look like in OOP:

 class Max implements Number { private final Number a; private final Number b; public Max(Number left, Number right) { this.a = left; this.b = right; } } 

And so I will use it:

 Number x = new Max(5, 9); 

See, I do not calculate the larger of the two numbers. I define that x is the larger of the two numbers. I'm not particularly worried about what is inside the object of the Max class and how exactly it implements the Number interface. I do not give instructions to the processor regarding this calculation. I just instantiate the object. This is very similar to def in Lisp .. In this sense, OOP is very similar to functional programming.

In contrast, static methods in OOP are the same as subprograms in C or assembler. They are not related to OOP and force us to write procedural code in object-oriented syntax. Here is the Java code:

 int x = Math.max(5, 9); 

This is completely wrong and should not be used in real object-oriented design.

Declarative style against imperative


Imperative programming "describes computations in terms of statements that change the state of a program.". Declarative programming, on the other hand, "expresses the logic of computation without describing its execution flow" (I quote Wikipedia). We have, in fact, spoken about this over the course of several previous pages. Imperative programming is similar to what computers do — sequential execution of instructions. Declarative programming is closer to the natural way of thinking, in which we have entities and relationships between them. Obviously, declarative programming is a more powerful approach, but the imperative approach is clearer to procedural programmers. Why is the declarative approach more powerful? Do not switch, and after a few pages we get to the bottom.

What does all this have to do with static methods? It doesn't matter if it's a static method or an object, we still have to write if (a> b) somewhere, right? Yes exactly. Both the static method and the object are just a wrapper over the if statement, which performs the task of comparing a with b. The difference is in how this functionality is used by other classes, objects, and methods. And this is a significant difference. Consider it by example.
Let's say I have an interval bounded by two integers, and an integer number that should fall into it .. I have to make sure that it is. Here is what I have to do if the max () method is static:

 public static int between(int l, int r, int x) { return Math.min(Math.max(l, x), r); } 

We need to create another static method, between (), which uses two existing static methods, Math.min () and Math.max (). There is only one way to do this - the imperative approach, since the value is calculated immediately. When I make a call, I immediately get the result:

 int y = Math.between(5, 9, 13); //  9 

I get the number 9 immediately after calling between (). When the call is made, my processor will immediately start working on this calculation. This is an imperative approach. And what then is the declarative approach?

Here, take a look:

 class Between implements Number { private final Number num; Between(Number left, Number right, Number x) { this.num = new Min(new Max(left, x), right); } @Override public int intValue() { return this.num.intValue(); } } 

Here is how I will use it:

 Number y = new Between(5, 9, 13); //   ! 

Feel the difference? She is extremely important. This style will be declarative, since I do not indicate to the processor that the calculations need to be performed immediately. I just determined what it was and left it up to the user to decide when (and if at all) to calculate the variable y using the intValue () method. Maybe it will never be calculated and my processor will never know that this number is 9 .. All I did was declare what y was. Just announced. I have not given any work to the processor yet. As stated in the definition, he expressed the logic without describing the process.

I already hear: “Okay, I understand you. There are two approaches - declarative and procedural, but why is the first one better than the second? ”I mentioned earlier that the declarative approach is obviously more powerful, but did not explain why. Now that we have looked at both approaches with examples, let's discuss the advantages of the declarative approach.

First, it is faster. At first glance, it may seem slower. But if you take a closer look, you will see that in fact it is faster, because performance optimization is completely in our hands. Indeed, creating an instance of the class Between takes more time than calling the static method between (), at least in most of the programming languages ​​available at the time of this writing. I really hope that in the near future we will have a language , in which object instantiation is as fast as a method call. But we have not come to him yet. That is why the declarative approach is slower ... when the execution path is simple and straightforward.

If we are talking about a simple call to a static method, then it will certainly be faster than creating an instance of an object and calling its methods. But if we have a lot of static methods, they will be consistently invoked when solving a problem, and not just to work on the results we really need. How about this:

 public void doIt() { int x = Math.between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

In this example, we calculate x regardless of whether we need its value or not. The processor in both cases will find the value 9. Will the following method, which uses a declarative approach, work as fast as the previous one?

 public void doIt() { Integer x = new Between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

I think the declarative code will be faster. It is better optimized. And it does not tell the processor what to do. On the contrary, it allows the processor to decide when and where the result is really needed - calculations are performed on demand.

The bottom line is that the declarative approach is faster because it is optimal. This is the first argument in favor of a declarative approach compared to an imperative in object-oriented programming. The imperative style is definitely not a place in OOP, and the first reason for this is performance optimization. It’s not necessary to say that the more you control the code optimization, the more it is followed. Instead of leaving the optimization of the computation process at the mercy of the compiler, virtual machine or processor, we do it ourselves.

The second argument is polymorphism. Simply put, polymorphism is the ability to break dependencies between blocks of code. Suppose I want to change the algorithm for determining whether a number falls within a certain interval. It is rather primitive in itself, but I want to change it. I don't want to use the Max and Min classes. And I want him to perform a comparison using if-then-else statements .. Here's how to do it declaratively:

 class Between implements Number { private final Number num; Between(int left, int right, int x) { this(new Min(new Max(left, x), right)); } Between(Number number) { this.num = number; } } 

This is the same class Between as in the previous example, but with an additional constructor. Now I can use it with another algorithm:

 Integer x = new Between( new IntegerWithMyOwnAlgorithm(5, 9, 13) ); 

This is probably not the best example, since the Between class is very primitive, but I hope you understand what I mean. The class Between is very easy to separate from the classes Max and Min, since they are classes. In object-oriented programming, an object is a full-fledged citizen, but a static method is not. We can pass an object as an argument to the constructor, but we cannot do the same with the static method. In OOP, objects are associated with objects, communicate with objects, and exchange data with them. To completely detach an object from other objects, we must make sure that it does not use the new operator in any of its methods (see Section 3.6), as well as in the main constructor.

Let me repeat: to completely decouple the object from other objects, you just have to make sure that the new operator does not apply to any of its methods, including the main constructor.

Can you do the same decoupling and refactoring with an imperative code snippet?

 int y = Math.between(5, 9, 13); 

No you can not. The static method between () uses two static methods, min () and max (), and you can’t do anything until you rewrite it completely. And how can you rewrite it? Pass the fourth parameter to a new static method?

How ugly will it look? I think very.

This is my second argument in favor of a declarative programming style - it reduces the cohesion of objects and makes it very elegant .. Not to mention that less cohesion means greater maintainability.

The third argument in favor of the superiority of the declarative approach over the imperative is that the declarative approach speaks of the results, and the imperative explains the only way to get them. The second approach is much less intuitive than the first. I must first "execute" the code in my head in order to understand what result to expect. Here is the imperative approach:

 Collection<Integer> evens = new LinkedList<>(); for (int number : numbers) { if (number % 2 == 0) { evens.add(number); } } 

To understand what this code does, I have to go through it, visualize this cycle .. In fact, I have to do what the processor does - go through the whole array of numbers and put the even ones in the new list. Here is the same algorithm, written in declarative style:

 Collection<Integer> evens = new Filtered( numbers, new Predicate<Integer>() { @Override public boolean suitable(Integer number) { return number % 2 == 0; } } ); 

This code snippet is much closer to the English language than the previous one. It reads like this: "evens is a filtered collection that includes only those elements that are even." I don't know exactly how the Filtered class creates the collection — whether it uses the for statement or something else. All I need to know while reading this code is that the collection has been filtered. Implementation details are hidden and behavior is expressed.

I realize that for some readers of this book it was easier to perceive the first fragment. It is a bit shorter and very similar to what you see daily in the code you are dealing with. I assure you that this is a matter of habit. This is a deceptive feeling. Start thinking in terms of objects and their behavior, not algorithms and their execution, and you will gain true perception. The declarative style directly concerns the objects and their behavior, and the imperative style - the algorithms and their execution.

If you think this code is ugly, try, for example, Groovy:

 def evens = new Filtered( numbers, { Integer number -> number % 2 == 0 } ); 

The fourth argument is code integrity. Take another look at the previous two fragments. Please note that in the second fragment we declare evens with one operator - evens = Filtered (...). This means that all the lines of code responsible for the calculation of this collection are next to each other and cannot be separated by mistake. On the contrary, in the first fragment there is no obvious glueing of the lines. You can easily change their order by mistake, and the algorithm will break.

In such a simple code snippet, this is a small problem, since the algorithm is obvious. But if the imperative code fragment is larger - say, 50 lines, it may be difficult to understand which lines of code are related to each other .. We discussed the temporal clutch problem a bit earlier - while discussing immutable objects .. The declarative programming style also helps to eliminate this clutch thanks to what maintainability improves.

Probably, there are still arguments, but I cited the most important, from my point of view, of the PLO. I hope I could convince you that the declarative style is what you need. Some of you may say, “Yes, I understand what you mean. I will combine declarative and imperative approaches where appropriate. I will use objects where it makes sense, and static methods when I need to quickly do something simple like calculating the larger of two numbers. ”“ No, you are wrong! ”I will answer you. You should not combine them .. Never apply an imperative style. This is not a dogma .. This has a quite pragmatic explanation.

Imperative style cannot be combined with declarative purely technical. When you start using the imperative approach, you are doomed - gradually all your code will become imperative.

Suppose we have two static methods - max () and min (). They perform small fast calculations, so we make them static. Now we need to create a larger algorithm to determine whether the number belongs to the interval .. This time we want to go in a declarative way - to create the class Between, and not the static method between (). Can we do that? Probably, yes, but in a surrogate way, and not as it should be. We cannot use constructors and encapsulation. And we are forced to make direct, explicit calls to static methods right inside the Between class. In other words, we cannot write purely object-oriented code if the reusable components are static methods.

- : , — . .

« ! — . — ?» … , . - , - ( ). , , — . , .. , . , , — , , , . , Apache Commons FileUtils.readLines(), . :

 class FileLines implements Iterable<String> { private final File file; public Iterator<String> iterator() { return Arrays.asList( FileUtils.readLines(this.file) ).iterator(); } } 

Now, to read all the lines from a text file, our application will have to do the following:

 Iterable<String> lines = new FileLines(f); 

The static method will be called only inside the FileLines class, and over time we will be able to get rid of it. Either this will never happen. But the point is that in our code, static methods will not be called anywhere, except for one place - the FileLines class. So we isolate the departed, which allows us to deal with them gradually.

Utility Classes


- , , ( -).. , java.lang.Math — -. Java, Ruby , , . ? . 1.1 , — . - , :

 class Math { private Math() { //   } public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

, -, , , . , , , .

- — - . - — — . , , . - — .. .

«»


«» — , , . , , . :

 class Math { private static Math INSTANCE = new Math(); private Math() {} public static Math getInstance() { return Math.INSTANCE; } public int max(int a, int b) { if (a < b) { return b; } return a; } } 

. Math, INSTANCE.. , getInstance(). , . INSTANCE — getInstance().

«» , . , . , . , , , , -, . - Math, , :

 class Math { private Math() {} public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

This is how the max () method will be used:

 Math.max(5, 9); // - Math.getInstance().max(5, 9); //  

What is the difference?It looks like the second line is just longer, but does the same thing. Why reinvent the singleton if we already had static methods and utility classes? I often ask this question during interviews with Java programmers. The first thing that I usually hear in response: "Singleton allows you to encapsulate the state." For example:

 class User { private static User INSTANCE = new User(); private String name; private User() {} public static User getInstance() { return User.INSTANCE; } public String getName() { return this.name; } public String setName(String txt) { this.name = txt; } } 

, . «, ». -, , - . .. , -: « ».. . .. -, , :

 class User { private static String name; private User() {} public static String getName() { return User.name; } public static String setName(String txt) { User.name = txt; } } 

- , . , ? ? , — , , - — , . , , setInstance() getInstance(). , . , :

 Math.getInstance().max(5, 9); 

Math. , Math — , . , Math , . , . , , , -, . , , Math.max() -. ? :

 Math math = new FakeMath(); Math.setInstance(math); 

The Singleton pattern provides the ability to replace the encapsulated static object, which allows you to test the object. The truth is this: a singleton is much better than a utility class just because it allows you to replace an encapsulated object. There is no object in the utility class - we cannot change anything. The utility class is an inseparable, hard-coded dependency - the purest evil in OOP.

So, what am I talking about? Singleton is better than utility class, but still is antipattern, and quite bad. Why?Because logically and technically a singleton is a global variable, neither more nor less. And in the PLO there is no global scope. Therefore, global variables are not the place. Here is a C program in which the variable is declared in the global scope:

 #include <stdio> int line = 0; void echo(char* text) { printf("[%d] %s\n", ++line, text); } 

Whenever we call echo (), the global variable line is incremented. Technically, the line variable is visible from each function and each line of code in the * .-file. It is visible globally. Praise to Java developers for not copying this feature from C. In Java, as in Ruby and in many other under-OOP languages, global variables are prohibited. Why? . . . . , . , , GOTO.

, , - Java, «».. - , . .
. .

« ? - you ask. — , , ?» , , , . - . What do we have? !

, .

, , .. . . , . , : , , . . , , , . , — , 2.1.

. .

Functional programming


: , ()? , , , .. , ? Lisp, Clojure Haskell Java C++?

, :

 class Max implements Number { private final int a; private final int b; public Max(int left, int right) { this.a = left; this.b = right; } @Override public int intValue() { return this.a > this.b ? this.a : this.b; } } 

:

 Number x = new Max(5, 9); 

Lisp , :

 (defn max (ab) (if (> ab) ab)) 

, ? Lisp .

, , — . - , - -, . , - Java, , Java , -. — , . .

, - . -, Java, ( ) , . .


, . — - . — - , — , , :

 names = new Sorted( new Unique( new Capitalized( new Replaced( new FileNames( new Directory( "/var/users/*.xml" ) ), "([^.]+)\\.xml", "$1" ) ) ) ); 

, , -. , 3.2. , names, , , . , , , . .

? , , , .

This is what I call composable decorators. The classes Directory, FileNames, Replaced, Capitalized, Unique, and Sorted are decorators, since their behavior is entirely due to the objects they encapsulate. They add some behavior to encapsulated objects. Their state coincides with the state of encapsulated objects.

Sometimes they provide the same interface as the objects they encapsulate (but this is not necessary). For example, Unique is Iterable, which also encapsulates a row iterator. However, FileNames is a row iterator that encapsulates a file iterator.
Most of the code in pure object-oriented software should be similar to the one given earlier. We have to compose the decorators into each other, and even a little more than that .. At some point, we call app.run (), and the whole pyramid of objects begins to react. There should be no procedural statements in the code at all, like if, for, switch and while. It sounds like utopia, but it is not utopia.

The if statement is provided by the Java language and is used by us in the procedural key, statement by statement. Why not create a replacement for Java language in which class would be If? Then instead of the following procedural code:

 float rate; if (client.age() > 65){ rate = 2.5; } else { rate = 3.0; } 

we would write such object-oriented code:

 float rate = new If( client.age() > 65, 2.5, 3.0 ); 

What about this?

 float rate = new If( new Greater(client.age(), 65), 2.5, 3.0 ); 

Finally, a recent improvement:

 float rate = new If( new GreaterThan( new AgeOf(client), 65 ), 2.5, 3.0 ); 

- . — , rate.

, , . if, for, switch while. If, For, Switch While. Feel the difference?

, . . . , .. , .
, - — .

? , : . , . . . , — .

: static — , , .

»More information about the book can be found on the publisher site.

For Habrozhiteley a 20% discount on coupon - Java

Source: https://habr.com/ru/post/418157/


All Articles