JavaRush /Java Blog /Random EN /Design patterns in Java
Viacheslav
Level 3

Design patterns in Java

Published in the Random EN group
Patterns or design patterns are an often overlooked part of a developer's job, making code difficult to maintain and adapt to new requirements. I suggest you look at what it is and how it is used in the JDK. Naturally, all the basic patterns in one form or another have been around us for a long time. Let's see them in this review.
Design Patterns in Java - 1
Content:

Templates

One of the most common requirements in vacancies is “Knowledge of patterns.” First of all, it’s worth answering a simple question - “What is a Design Pattern?” Pattern is translated from English as “template”. That is, this is a certain pattern according to which we do something. The same is true in programming. There are some established best practices and approaches to solving common problems. Every programmer is an architect. Even when you create only a few classes or even one, it depends on you how long the code can survive under changing requirements, how convenient it is to be used by others. And this is where knowledge of templates will help, because... This will allow you to quickly understand how best to write code without rewriting it. As you know, programmers are lazy people and it’s easier to write something well right away than to redo it several times) Patterns may also seem similar to algorithms. But they have a difference. The algorithm consists of specific steps that describe the necessary actions. Patterns only describe the approach, but do not describe the implementation steps. The patterns are different, because... solve different problems. Typically the following categories are distinguished:
  • Generative

    These patterns solve the problem of making object creation flexible

  • Structural

    These patterns solve the problem of effectively building connections between objects

  • Behavioral

    These patterns solve the problem of effective interaction between objects

To consider examples, I suggest using the online code compiler repl.it.
Design Patterns in Java - 2

Creational patterns

Let's start from the beginning of the life cycle of objects - with the creation of objects. Generative templates help create objects more conveniently and provide flexibility in this process. One of the most famous is " Builder ". This pattern allows you to create complex objects step by step. In Java, the most famous example is StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Another well-known approach to creating an object is to move the creation into a separate method. This method becomes, as it were, an object factory. That's why the pattern is called " Factory Method". In Java, for example, its effect can be seen in the class java.util.Calendar. The class itself Calendaris abstract, and to create it the method is used getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
This is often because the logic behind object creation can be complex. For example, in the case above, we access the base class Calendar, and a class is created GregorianCalendar. If we look at the constructor, we can see that different implementations are created depending on the conditions Calendar. But sometimes one factory method is not enough. Sometimes you need to create different objects so that they fit together. Another template will help us with this - “ Abstract factory ”. And then we need to create different factories in one place. At the same time, the advantage is that implementation details are not important to us, i.e. it doesn’t matter which specific factory we get. The main thing is that it creates the right implementations. Super example:
Design Patterns in Java - 3
That is, depending on the environment (operating system), we will get a certain factory that will create compatible elements. As an alternative to the approach of creating through someone else, we can use the " Prototype " pattern. Its essence is simple - new objects are created in the image and likeness of already existing objects, i.e. according to their prototype. In Java, everyone has come across this pattern - this is the use of an interface java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
As you can see, the caller does not know how the clone. That is, creating an object based on a prototype is the responsibility of the object itself. This is useful because it does not tie the user to the implementation of the template object. Well, the very last one on this list is the “Singleton” pattern. Its purpose is simple - to provide a single instance of the object for the entire application. This pattern is interesting because it often shows multithreading problems. For a more in-depth look, check out these articles:
Design Patterns in Java - 4

Structural patterns

With the creation of objects it became clearer. Now is the time to look at structural patterns. Their goal is to build easy-to-support class hierarchies and their relationships. One of the first and well-known patterns is “ Deputy ” (Proxy). The proxy has the same interface as the real object, so it makes no difference for the client to work through the proxy or directly. The simplest example is java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
As you can see, in the example we have original - this is the one HashMapthat implements the interface Map. We next create a proxy that replaces the original one HashMapfor the client part, which calls the putand methods get, adding our own logic during the call. As we can see, interaction in the pattern occurs through interfaces. But sometimes a substitute is not enough. And then the " Decorator " pattern can be used. A decorator is also called a wrapper or wrapper. Proxy and decorator are very similar, but if you look at the example, you will see the difference:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
Unlike a proxy, a decorator wraps itself around something that is passed as input. A proxy can both accept what needs to be proxied and also manage the life of the proxied object (for example, create a proxied object). There is another interesting pattern - “ Adapter ”. It is similar to a decorator - the decorator takes one object as input and returns a wrapper over this object. The difference is that the goal is not to change the functionality, but to adapt one interface to another. Java has a very clear example of this:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
At the input we have an array. Next, we create an adapter that brings the array to the interface List. When working with it, we are actually working with an array. Therefore, adding elements will not work, because... The original array cannot be changed. And in this case we will get UnsupportedOperationException. The next interesting approach to developing class structure is the Composite pattern . It is interesting in that a certain set of elements using one interface are arranged in a certain tree-like hierarchy. By calling a method on a parent element, we get a call to this method on all necessary child elements. A prime example of this pattern is UI (be it java.awt or JSF):
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
As we can see, we have added a component to the container. And then we asked the container to apply the new orientation of the components. And the container, knowing what components it consists of, delegated the execution of this command to all child components. Another interesting pattern is the “ Bridge ” pattern. It is called this because it describes a connection or bridge between two different class hierarchies. One of these hierarchies is considered an abstraction and the other an implementation. This is highlighted because the abstraction itself does not perform actions, but delegates this execution to the implementation. This pattern is often used when there are "control" classes and several types of "platform" classes (for example, Windows, Linux, etc.). With this approach, one of these hierarchies (abstraction) will receive a reference to objects of another hierarchy (implementation) and will delegate the main work to them. Because all implementations will follow a common interface, they can be interchanged within the abstraction. In Java, a clear example of this is java.awt:
Design Patterns in Java - 5
For more information, see the article " Patterns in Java AWT ". Among the structural patterns, I would also like to note the “ Facade ” pattern. Its essence is to hide the complexity of using the libraries/frameworks behind this API behind a convenient and concise interface. For example, you can use JSF or EntityManager from JPA as an example. There is also another pattern called " Flyweight ". Its essence is that if different objects have the same state, then it can be generalized and stored not in each object, but in one place. And then each object will be able to reference a common part, which will reduce memory costs for storage. This pattern often involves pre-caching or maintaining a pool of objects. Interestingly, we also know this pattern from the very beginning:
Design Patterns in Java - 6
By the same analogy, a pool of strings can be included here. You can read the article on this topic: " Flyweight Design Pattern ".
Design Patterns in Java - 7

Behavioral patterns

So, we figured out how objects can be created and how connections between classes can be organized. The most interesting thing left is to provide flexibility in changing the behavior of objects. And behavioral patterns will help us with this. One of the most frequently mentioned patterns is the " Strategy " pattern. This is where the study of patterns in the book “ Head First. Design Patterns ” begins. Using the “Strategy” pattern, we can store inside an object how we will perform the action, i.e. the object inside stores a strategy that can be changed during code execution. This is a pattern we often use when using a comparator:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Before us - TreeSet. It has the behavior of TreeSetmaintaining the order of elements, i.e. sorts them (since it is a SortedSet). This behavior has a default strategy, which we see in the JavaDoc: sorting in "natural ordering" (for strings, this is lexicographical order). This happens if you use a parameterless constructor. But if we want to change the strategy, we can pass Comparator. In this example, we can create our set as new TreeSet(comparator), and then the order of storing elements (storage strategy) will change to the one specified in the comparator. Interestingly, there is almost the same pattern called " State ". The “State” pattern says that if we have some behavior in the main object that depends on the state of this object, then we can describe the state itself as an object and change the state object. And delegate calls from the main object to the state. Another pattern known to us from studying the very basics of the Java language is the “ Command ” pattern. This design pattern suggests that different commands can be represented as different classes. This pattern is very similar to the Strategy pattern. But in the Strategy pattern, we were redefining how a specific action would be performed (for example, sorting in TreeSet). In the “Command” pattern, we redefine what action will be performed. The pattern command is with us every day when we use threads:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
As you can see, command defines an action or command that will be executed in a new thread. It is also worth considering the “ Chain of responsibility ” pattern. This pattern is also very simple. This pattern says that if something needs to be processed, then you can collect handlers in a chain. For example, this pattern is often used in web servers. At the input, the server has some request from the user. This request then goes through the processing chain. This chain of handlers includes filters (for example, do not accept requests from a blacklist of IP addresses), authentication handlers (allow only authorized users), a request header handler, a caching handler, etc. But there is a simpler and more understandable example in Java java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
As you can see, Handlers are added to the list of logger handlers. When a logger receives a message for processing, each such message passes through a chain of handlers (from logger.getHandlers) for that logger. Another pattern we see every day is “ Iterator ”. Its essence is to separate a collection of objects (i.e. a class representing a data structure. For example, List) and traversal of this collection.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
As you can see, the iterator is not part of the collection, but is represented by a separate class that traverses the collection. The user of the iterator may not even know what collection it is iterating over, i.e. what collection is he visiting? The Visitor pattern is also worth considering . The visitor pattern is very similar to the iterator pattern. This pattern helps you bypass the structure of objects and perform actions on these objects. They differ rather in concept. The iterator traverses the collection so that the client using the iterator doesn't care what the collection is inside, only the elements in the sequence are important. The visitor means that there is a certain hierarchy or structure of the objects that we visit. For example, we can use separate directory processing and separate file processing. Java has an out-of-the-box implementation of this pattern in the form java.nio.file.FileVisitor:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
Sometimes there is a need for some objects to react to changes in other objects, and then the “Observer” pattern will help us . The most convenient way is to provide a subscription mechanism that allows some objects to monitor and respond to events occurring in other objects. This pattern is often used in various Listeners and Observers that react to different events. As a simple example, we can recall the implementation of this pattern from the first version of JDK:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
There is another useful behavioral pattern - “ Mediator ”. It is useful because in complex systems it helps to remove the connection between different objects and delegate all interactions between objects to some object, which is an intermediary. One of the most striking applications of this pattern is Spring MVC, which uses this pattern. You can read more about this here: " Spring: Mediator Pattern ". You can often see the same in examples java.util.Timer:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
The example looks more like a command pattern. And the essence of the "Mediator" pattern is hidden in the implementation of Timer'a. Inside the timer there is a task queue TaskQueue, there is a thread TimerThread. We, as clients of this class, do not interact with them, but interact with Timerthe object, which, in response to our call to its methods, accesses the methods of other objects of which it is an intermediary. Externally it may seem very similar to "Facade". But the difference is that when a Facade is used, the components do not know that the facade exists and talk to each other. And when "Mediator" is used, the components know and use the intermediary, but do not contact each other directly. It is worth considering the “ Template Method ” pattern. The pattern is clear from its name. The bottom line is that the code is written in such a way that users of the code (developers) are provided with some algorithm template, the steps of which are allowed to be redefined. This allows code users not to write the entire algorithm, but to think only about how to correctly perform one or another step of this algorithm. For example, Java has an abstract class AbstractListthat defines the behavior of an iterator by List. However, the iterator itself uses leaf methods such as: get, set, remove. The behavior of these methods is determined by the developer of the descendants AbstractList. Thus, the iterator in AbstractList- is a template for the algorithm for iterating over a sheet. And developers of specific implementations AbstractListchange the behavior of this iteration by defining the behavior of specific steps. The last of the patterns we analyze is the “ Snapshot ” (Momento) pattern. Its essence is to preserve a certain state of an object with the ability to restore this state. The most recognizable example from the JDK is object serialization, i.e. java.io.Serializable. Let's look at an example:
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Design Patterns in Java - 8

Conclusion

As we saw from the review, there are a huge variety of patterns. Each of them solves its own problem. And knowledge of these patterns can help you understand in time how to write your system so that it is flexible, maintainable and resistant to change. And finally, some links for a deeper dive: #Viacheslav
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION