The book "Elegant Objects. Java Edition »

    imageHi, 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 it to be evil. For a novice programmer, this volume can become an enlightenment or shock, and for an experienced programmer it is a must-read.

    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 just as big, if not even more of a problem in OOP, than having a 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 the purest 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":

    classWebPage {
      publicstatic 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):

    classWebPage{
      privatefinal 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 worsen 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:

    intmax(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 (a b)
         (if (> a b) a b))

    Can you tell where the execution of this code begins and ends? Not. We do not know in what way the processor will get the result, nor exactly 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 (max59))

    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. Note that 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:

    classMaximplementsNumber{
      privatefinal Number a;
      privatefinal Number b;
      publicMax(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, in fact, spoke about it throughout 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 intbetween(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:

    classBetweenimplementsNumber{
      privatefinal Number num;
      Between(Number left, Number right, Number x) {
          this.num = new Min(new Max(left, x), right);
      }
      @OverridepublicintintValue(){
          returnthis.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 computed 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 better than the second? ”I mentioned earlier that the declarative approach is obviously more powerful, but did not explain why. Now that we have considered 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, since 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 programming languages ​​available at the time of this writing. I really hope that in the near future we will have a language in which instantiating an object will be as fast as calling a method. 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 called when solving a problem, and not just to work on the results we really need. How about this:

    publicvoid 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 the declarative approach, work as fast as the previous one?

    publicvoid doIt() {
        Integer x = newBetween(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 with 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 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:

    classBetweenimplementsNumber{
      privatefinal 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 = newBetween(
       new IntegerWithMyOwnAlgorithm(5, 9, 13)
    );

    This is probably not the best example, because 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.

    Here 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 the fact 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 is doing, 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
           publicboolean suitable(Integer number) {
               return number % 2 == 0;
           }
         }
    );

    This code fragment 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:

    defevens= newFiltered(
       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 can 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 completely pragmatic explanation.

    The imperative style cannot be combined with the 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 if 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 they are forced to make immediate, explicit calls to static methods right inside the Between class. In other words, we will not be able to write a purely object-oriented code if the reusable components are static methods.

    Static methods resemble object-oriented cancer: once you allow them to settle in the code, we cannot get rid of them - their colony will only grow. Just bypass them in principle.

    “But they are everywhere! - you exclaim. “What to do?” What can I say ... you have problems, like we all have. There are thousands of object-oriented libraries, almost entirely consisting of utility classes (we will discuss them in the next section). Here, as with a tumor, the best tool is a knife. Do not use such programs if you can afford it .. However, in most cases you will not be able to afford to use a knife, since these libraries are very popular and provide useful functionality. In this case, the best thing you can do is isolate the tumor by creating your own classes that wrap static methods so that your code works exclusively with objects. For example, in the Apache Commons library there is a static FileUtils.readLines () method that reads all lines from a text file.

    classFileLinesimplementsIterable<String> {
      privatefinal 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 need to do the following:

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

    The call to the static method will occur only inside the FileLines class, and over time we will be able to get rid of it. Either this will never happen. But the bottom line 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


    The so-called utility classes are actually not classes, but only a set of static methods used by other classes for convenience (they are also known as helper methods). For example, the class java.lang.Math is a classic example of a utility class. Such generations are very popular in Java, Ruby and, unfortunately, in almost all modern programming languages. Why are they not classes? Because of them it is impossible to instantiate objects. In section 1.1, we discussed the difference between an object and a class and came to the conclusion that the class is a factory of objects. The utility class is not a factory, for example:

    classMath {
      privateMath() {
         // намеренно пустой
      }
      publicstaticintmax(int a, int b) {
          if (a < b) {
            return b;
          }
            return a;
      }
    }

    A good practice for those who use utility classes is to create a private constructor, as in the example, to avoid creating an instance of the class. Since the constructor is private, no one except the class methods can create an instance of the class.

    Utility Classes - the triumph of procedural programmers in object-oriented programming. A utility class is not just a terrible thing, like a static method - it is a bunch of terrible things. All bad words spoken about static methods can be repeated with multiple amplification. Utility classes are a terrible anti-pattern in OOP. Stay away from them.

    Pattern "Singleton"


    The Singleton Pattern is a popular technique that claims to be a substitute for static methods. Indeed, there will be only one static method in the class, and the singleton will look almost like a real object. However, it is not named:

    classMath {
      privatestatic Math INSTANCE = new Math();
      privateMath() {}
      publicstatic Math getInstance() {
          return Math.INSTANCE;
      }
      publicintmax(int a, int b) {
          if (a < b) {
            return b;
          }
          return a;
      }
    }

    The above is a typical example of a singleton. There is a single instance of the Math class called INSTANCE .. Everyone can access it simply by calling getInstance (). The constructor is made private to prevent direct instantiation of objects of this class. The only way to access INSTANCE is to call getInstance ().

    Singleton is known as a design pattern, but in reality it is a terrible anti-pattern. There are many reasons why this is a bad programming technique. I will give only some of them concerning static methods. It would, of course, be easier if we first discussed what makes Singleton different from the utility class that we just talked about. Here is what the Math utility class would look like, which does the same thing as the singleton cited earlier:

    classMath {
      privateMath() {}
      publicstaticintmax(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 I usually hear in response: "Singleton allows you to encapsulate the state." For example:

    classUser {
      privatestatic User INSTANCE = new User();
      private String name;
      privateUser() {}
      publicstatic User getInstance() {
          return User.INSTANCE;
      }
      public String getName() {
          returnthis.name;
      }
      public String setName(String txt) {
          this.name = txt;
      }
    }

    This is a terrible piece of code, but I have to cite it as an illustration of my arguments. This singleton literally means "user currently using the system." This approach is very popular in many web frameworks, where there are user singletons, web sessions, and so on. So, the typical answer to my question about the difference between a singleton and a utility class: “Singleton encapsulates a state” .. But this is not correct answer. The purpose of a singleton is not to store state .. Here is a utility class that does the same thing as the singleton mentioned earlier:

    classUser {
      privatestatic String name;
      privateUser() {}
      publicstatic String getName() {
          return User.name;
      }
      publicstatic String setName(String txt) {
          User.name = txt;
      }
    }

    This utility class stores state, and there is no difference between it and the mentioned singleton. So what is the problem? And what is the correct answer? The only correct answer is that a singleton is a dependency that can be broken, and a utility class is a tightly-programmed close relationship that cannot be broken. In other words, the advantage of singletons is that they can add the setInstance () method along with getInstance (). This answer is correct, although I rarely hear it. Suppose I use singleton like this:

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

    My code is linked to the Math class. In other words, the Math class is a dependency I rely on. Without this class, the code will not work, and to test it, I will have to leave the Math class available to be able to execute queries. In the case of this particular class, this problem is small, since it is very primitive. However, if a singleton is big, then I may have to use mocking or replace it with something that is better suited for testing. Simply put, I don’t want the Math.max () method to run while the unit test is running. How can I do it? That's how:

    Mathmath = 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. A 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 do not belong here. Here is a C program in which the variable is declared in the global scope:

    #include <stdio>intline = 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? Because they have nothing to do with the PLO. This is a purely procedural possibility. Global variables unambiguously violate the principle of encapsulation. They are just awful. I hope I don’t have to explain this in this book anymore. It seems to me that global variables are as bad as the GOTO operator.

    However, despite all the arguments against global variables, someone found a way to introduce them into Java, thereby creating the Singleton pattern .. This is simply a mockery of the principles of object-oriented design, made possible by the presence of static methods. These methods technically allow such a scam.
    Never use singletons. Don't even think about it.

    “How to replace them? - you ask. “If we need something to be available to many classes within the entire software product, what can we do?” Let's say we really need most of the classes to know which user is currently logged in. We do not have utility classes and singltons. What do we have? Encapsulation!

    Just encapsulate the user in all the objects in which he can come in handy.

    Everything your class needs to work must be passed through the constructor and encapsulated inside the class .. That's all. Without exception. The object should not affect anything other than its encapsulated properties. You can say that you have to encapsulate too much: the connections to the database, the logged in user, the command line arguments, etc. Yes, indeed, all this may be too much, if the class is too large and not sufficiently integral. If you need to encapsulate too much, recycle the class - reduce it, as discussed in section 2.1.

    But never use singleton. There are no exceptions to this rule.

    Functional programming


    I often hear the following argument: if objects are small and immutable and static methods are not involved, why not use functional programming (FP)? Indeed, if objects are as elegant as recommended in this book, then they are very similar to functions .. So, why do we need objects? Why not just use Lisp, Clojure or Haskell instead of Java or C ++?

    Here is a class representing the algorithm for determining the larger of two numbers:

    classMaximplementsNumber{
      privatefinalint a;
      privatefinalint b;
      publicMax(int left, int right){
          this.a = left;
          this.b = right;
      }
      @OverridepublicintintValue(){
          returnthis.a > this.b ? this.a : this.b;
      }
    }

    Here is how we should apply it:

    Number x = new Max(5, 9);

    This is how we would define a function in Lisp that would do the same:

    (defn max
         (a b)
         (if (> a b) a b))

    So why use objects? Lisp code is much shorter.

    OOP is more expressive and has great capabilities, since it operates with objects and methods, while the OP is only functions. In some FP languages ​​there are also objects, but I would call them OOP languages ​​with FP capabilities, and not vice versa. I also believe that lambda expressions in Java, being a move toward the OP, make Java more friable, knocking us off the true OOP path. OP is a great paradigm, but OOP is better. Especially with proper use.

    It seems to me that in an ideal OOP language, we would have classes with functions inside. Not microprocedure methods, as now in Java, but real (in the sense of the functional paradigm) functions that have a single exit point. That would be perfect.

    Compiled Decorators


    It seems that the term I came up with. Composable decorators are simply wrapper objects over other objects. They are decorators - a known object-oriented design pattern - but they become composable when we combine them into multi-layered structures, for example:

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

    Such code, from my point of view, looks very clean and object-oriented. It is exclusively declarative, as explained in section 3.2. It does nothing, it only declares the names object, which is a sorted collection of unique uppercase strings representing the names of the files in the directory, modified by a specific regular expression. I simply explained what this object is, without saying a word about how it works. I just announced it.

    Do you think this code is clean and easy to understand? I hope that yes, given all that we talked about earlier.

    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 the 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 line 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, such as 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 Java replacement language that has an If class? 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 = newIf(
      client.age() > 65,
      2.5, 3.0
    );

    What about that?

    float rate = newIf(
      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
    );

    This is pure object-oriented and declarative code. He does nothing - he simply declares what rate is.

    From my point of view, in a pure OOP, operators inherited from procedural languages ​​like C are not needed. C, if, for, switch, and while are not needed. We need the If, ​​For, Switch, and While classes. Feel the difference?

    We have not yet reached such languages, but sooner or later we will definitely get there. I am sure about that. In the meantime, try to stay away from long methods and complicated procedures. Design microclasses so that they are buildable. Ensure that they can be reused as elements of the composition in larger objects.
    I would say that object-oriented programming is the assembly of large objects from smaller ones.

    What does this have to do with static methods? I am sure you have already understood: static methods cannot be put together in any way. They make impossible everything that I said and showed earlier. We cannot collect large objects from smaller ones using static methods. These methods contradict the layout idea. This is another reason why static methods are pure evil.

    In conclusion: nowhere and never use the static keyword in your code - this will do yourself and those who will use your code a great service.

    »More information on the book is available on the publisher's website.

    For Habrozhiteley 20% discount on the coupon - Java

    Also popular now: