fbpx
Modern view of design patterns in Java

Для начала, краткое вступления на русском. Это первая моя полноценная статья на английском (я когда-то вел блог Java development tips на английском, но это было так давно, что не может считаться), написанная по мотивам одноименного доклада, с которым я выступал на JavaDay Kiev и Joker в 2015 году. С обновленным докладом на эту же тему я собираюсь поехать в этом году на конференцию Devoxx в Бельгию. Если вас интересует более функциональный взгляд на шаблоны проектирования, то рекомендую прочитать дополнительно серию статей Mario Fusco. Ну и не судите строго, я полностью открыт к здравой критике. 🙂

I think every Java developer knows something about design patterns. Some of them even tried to read famous book “Design Patterns: Elements of Reusable Object-Oriented Software” and not fall asleep, others prefer lightweight version with beautiful girl on the cover: “Head First Design Patterns”. At least questions related to design patterns are common on every interview for developer position, so remaining part of developers community got some basic knowledge from articles and google.

Every design pattern represents some problem with full context and common solutions to this problem with samples in popular OOP oriented languages (this is long discussion “Is Java completely OOP or not” for another article). Yes, for most of design patterns, as well as problems behind them, are valid only for OOP world and in functional world they are useless. But Java has been significantly improved over last 10 years and probably we need to review design patterns and especially solutions to check whether anything changed there.

There are many design patterns and nobody likes long articles. That is why I took only several of them for this article. Full set of samples is available on GitHub, I also have a talk on this topic (slides in English on Slideshare and video in Russian from JavaDay Kiev conferences). If you like the idea and have good samples for other design patterns, pull requests are welcome!

Singleton

The first pattern I would like to review is the old and the famous one: Singleton (creational group). It’s goal is to ensure a class has only one instance and provide a global access point to this instance. Experienced Java developers know that most popular implementation from books and articles with double-checked locking is broken and at the same time additional instance may be created without any issues with Reflection API (check this article as an example of “war for valid Singleton”). But in modern Java world this pattern is DEAD and there is no need to worry about it anymore. Even more, Singleton became the most popular anti-pattern. The reason is in Dependency Injection principle that addresses deeper problem: objects was responsible for their dependencies creation. When this responsibility is delegated to the third party like DI container (Spring, Guice, JavaEE) then almost all long-lived objects are configured in single place and number of instances is controlled there as well. So, we have singletons by default and initial problem of this design pattern is not more actual.

Builder

Pattern Builder (creational group) simplifies creation of composite objects making it more flexible and configurable for client needs. It is especially popular in our days because many libraries, frameworks, components, services are designed to have fluent and clear API. Java language haven’t made any progress in object construction area, so we still have only constructors, static methods and setters (setters return void making impossible chain of several invocations). Let’s start from the simplest solution: Lombok. This library modifies bytecode of your class adding some parts of the code that you configure using annotations. @Builder annotation is used to generate different types of builders. So you just mark your class with this annotation, add some additional configuration to have even more powerful options and that’s it. Easy and quick, no manual code! If you want to see the full power of Lombok in practice, take a look at this video from JEEConf 2016.

But tools never know your business logic and could generate only standart things for you. When you design you own API it is not always what you really need. So, many people still write builder implementation themselves. Let’s review an example, where you want to create configurable Cluster object as a part of your API:

Cluster cluster = Cluster.runtimeBuilder()
    .addContactPoints("localhost").withPort(3165)
    .withRetryAttempts(3)
    .withoutMetrics().build();

You see here that methods of the builder are named according to Cluster DSL. Also, in this sample we will use 2 types of buildes, so here ‘runtime builder’ is demonstrated. Cluster itself is simple POJO with several properties (I skipped part with getters and setters):

public class Cluster {
   private String[] hosts;
   private int port;
   private int retryAttempts;
   private boolean metricsEnabled;

   public static StoringBuilder storingBuilder() {
      return new StoringBuilder();
   }

   public static RuntimeBuilder runtimeBuilder() {
      return new RuntimeBuilder();
   }

   public Cluster(String[] hosts, int port) {
      this.hosts = hosts;
      this.port = port;
   }

   //default constructor and setters are here
}

First type of builder is ‘runtime’. It creates Cluster instance as a field and then just set needed properties on it adding corresponding business rules. Method build() in this case simply returns field value. Pros of ‘runtime’ builder are in reduced amount of duplicated code to store the same set of fields as Cluster has, but at the same time ‘runtime’ builder may be used only once because the next invocation of build() method will return the same value. This side effect may be fixed if last statement of build() method will create new instance and assign it to field.

public static class RuntimeBuilder {
   private Cluster cluster = new Cluster();

   public RuntimeBuilder addContactPoints(String... hosts) {
      if (hosts == null || hosts.length == 0) {
         throw new IllegalArgumentException("Hosts must be set!");
      }
      cluster.setHosts(hosts);
      return this;
   }

   public RuntimeBuilder withPort(int port) {
      cluster.setPort(port);
      return this;
   }

   public RuntimeBuilder withRetryAttempts(int retryAttempts) {
      cluster.setRetryAttempts(retryAttempts);
      return this;
   }

   public RuntimeBuilder withoutMetrics() {
      cluster.setMetricsEnabled(false);
      return this;
   }

   public Cluster build() {
      return cluster;
   }
}

Another type of builder is ‘storing’ builder that is used as canonical implementation in most code samples on this topic. It is completely reusable and stores state to independent set of fields. Method build() creates new instance on every invocation, so no side effects present.

public static class StoringBuilder {
   private String[] hosts;
   private int port;
   private int retryAttempts;
   private boolean metricsEnabled;

   public StoringBuilder addContactPoints(String... hosts) {
      if (hosts == null || hosts.length == 0) {
         throw new IllegalArgumentException("Hosts must be set!");
      }
      this.hosts = hosts;
      return this;
   }

   public StoringBuilder withPort(int port) {
      this.port = port;
      return this;
   }

   public StoringBuilder withRetryAttempts(int retryAttempts) {
      this.retryAttempts = retryAttempts;
      return this;
   }

   public StoringBuilder withoutMetrics() {
      this.metricsEnabled = false;
      return this;
   }

   public Cluster build() {
      Cluster cluster = new Cluster(hosts, port);
      cluster.setMetricsEnabled(metricsEnabled);
      cluster.setRetryAttempts(retryAttempts);
      return cluster;
   }
}

Proxy

Let’s move to the next pattern. Proxy (structural group) provides a surrogate or placeholder for another object to control access to it. I think Proxy is the most popular pattern because almost all popular frameworks are using it and especially after AOP became very popular. High demand and wide usage caused creation of many libraries that allow easily create proxy on the fly: cglib, javassist, byte buddy, etc. Some examples of real life problems solved by Proxy:

  • Role based access control for service methods invocation.
  • Detailed logging with performance information.
  • Caching of method results.
  • Transparent remote invocation.
  • Transaction management.

You could create Proxy just for fun with functional interfaces but it is very rare case in the real life. To do so you cast your additional logic implementation to Consumer or even implement Consumer interface directly, then use andThen method to pass real method reference. As a result you have consumer that first invokes additional logic and then real method:

public interface OrderService {
   void processOrder(Order order);
}

public class RealOrderService implements OrderService {
   @Override
   public void processOrder(Order order) {
      System.out.println("Order processed: " + order);
   }
}

public class PermissionChecker {
   private final ThreadLocal<Long> currentUser = new ThreadLocal<>();

   public void setCurrentUser(long userId) {
      currentUser.set(userId);
   }

   public void checkPermission(Order order) {
      if (order.getUserId() != currentUser.get()) {
         throw new IllegalStateException("Order for another user can't be processed: " + order);
      }
   }
}

public class ModernProxyClient {
   public static void main(String[] args) {
      Order order = new Order(5L);
      order.putItem("XP", 2);

      RealOrderService service = new RealOrderService();
      PermissionChecker checker = new PermissionChecker();
      Consumer<Order> processing = ((Consumer<Order>) checker::checkPermission)
         .andThen(service::processOrder);

      checker.setCurrentUser(5);
      processing.accept(order);
      checker.setCurrentUser(2);
      processing.accept(order);
   }
}

Decorator

Decorator (structural group) is very similar to Proxy but usually used to extend functionality of existing class with additional responsibilities. Everything said for Proxy from technical perspective is also valid for Decorator. I would like to share additional technique for the simplest way of wrapping existing code into another block of code:

public interface DigitCounter {
   int count(String str);
}

public class ModernDecoratorClient {
   public static void main(String[] args) {
      DigitCounter counter = wrap(new NaiveDigitCounter());
      int digitsCount = counter.count("fd6j78fh19kj");
      System.out.println(digitsCount + " digits found");
   }

   public static DigitCounter wrap(DigitCounter counter) {
      return s -> {
         long startTime = System.currentTimeMillis();
         int count = counter.count(s);
         long endTime = System.currentTimeMillis();
         System.out.println("Counting took " + (endTime - startTime) + " ms");
         return count;
      }
   }
}

Factory Method

Factory Method (creational group) is very famous design pattern, especially on interviews when candidate asked to explain how Factory Method differs from Abstract Factory. The idea is very simple: we define an interface for object creation but let subclasses to decide which concrete class to instantiate, so client is not coupled to particular implementation. As well as Singleton this pattern was invented in epoch before Dependency Injection when every object was responsible for his dependencies. In our days concrete implementation may be chosen by DI container and injected to the object. But there are still some places where you need to create objects in runtime and Factory Method is still very useful.

In Java 8 you have an option to simplify cases when object creation logic is trivial and only particular constructor is invoked with passed parameters, then you can pass reference to constructor as Method Factory implementation. Also there is no need to create new interface for every class, you can use functional interfaces instead: Supplier if there is no params and Function if only one param is passed. Both this cases are shown in following sample:

public class ModernFactoryMethodClient {
   public static void main(String[] args) {
      DocumentFactory factory = JsonDocument::new;
      printUserDetails(factory.create("USER"));

      Function<String, Document> plainFactory = JsonDocument::new;
      printUserDetails(plainFactory.apply("USER"));
   }

   private static void printUserDetails(Document document) {
      document.addField("name", "Mikalai");
      document.addField("surname", "Alimenkou");
      System.out.println(document);
   }
}

public interface DocumentFactory {
   Document create(String name);
}

public interface Document {
   String getName();

   void addField(String name, String value);

   String toString();
}

public class JsonDocument implements Document {
   private final String name;
   private final Map<String, String> fields = new LinkedHashMap<>();

   public JsonDocument(String name) {
      this.name = name;
   }

   @Override
   public String getName() {
      return name;
   }

   @Override
   public void addField(String name, String value) {
      fields.put(name, value);
   }

   @Override
   public String toString() {
      return fields.entrySet().stream()
         .map(e -> e.getKey() + ": " + e.getValue())
         .collect(Collectors.joining(",\n", "{\n", "\n}"));
   }
}

Command and Strategy

Command pattern (behavioral group) encapsulates request as an object letting parametrize clients with different requests. It was born because lack of options to manipulate methods as independent entities in Java. Solution is usually was to use single method objects instead. From Java 8 it is possible to use combination of method reference and functional interfaces. So the problem is not actual anymore (except cases when your request has group of related methods like do/undo/check):

public class Document implements Editor {
   @Override
   public void bold() {
      System.out.println("Bold text...");
   }

   @Override
   public void italic() {
      System.out.println("Italic text...");
   }

   @Override
   public void underline() {
      System.out.println("Underline text...");
   }
}

public class ModernMacro {
   private final List<Command> commands = new ArrayList<>();

   public ModernMacro record(Command action) {
      commands.add(action);
      return this;
   }

   public void run() {
      commands.stream().forEach(Command::execute);
   }
}

public class ModernCommandClient {
   public static void main(String[] args) {
      Document editor = new Document();
      ModernMacro macro = new ModernMacro();

      macro.record(editor::bold)
         .record(editor::italic)
         .record(editor::underline)
         .run();
   }
}

Strategy pattern ((behavioral group) is used to define family of algorithms, encapsulate each one and make them interchangeable. In most cases strategy is represented with single method, so situation with this pattern is very similar to Command: it is possible to use method reference directly, inline lambda expression (especially useful for testing) or casting between different functional interfaces. Here is an example:

public class ModernStrategyClient {
   private final ToIntFunction calculator;

   public ModernStrategyClient(ToIntFunction calculator) {
      this.calculator = calculator;
   }

   public static void main(String[] args) {
      Position position = new Position(3, 1000);
      new ModernStrategyClient(TrafficCalculationStrategies::calculateTopTrafficOnly).process(position);
      new ModernStrategyClient(TrafficCalculationStrategies::calculateTrafficForPage).process(position);

      // for unit tests
      new ModernStrategyClient(pos -> 15).process(position);
   }

   public void process(Position position) {
      int traffic = calculator.applyAsInt(position);
      System.out.println("Traffic is " + traffic + " for position " + position);
   }
}

public interface TrafficCalculator {
   int calculate(Position position);
}

public final class TrafficCalculationStrategies {
   private static final int PAGE_SIZE = 10;
   private static final double[] TOP_RANKS = {0.5, 0.3, 0.1, 0.05, 0.05};

   private TrafficCalculationStrategies() {}

   public static int calculateTrafficForPage(Position position) {
      int rank = position.getRank();
      if (rank < 0) {
         return 0;
      }
      int page = rank / PAGE_SIZE;
      return position.getMediaValue() / (PAGE_SIZE * (page + 1));
   }

   public static int calculateTopTrafficOnly(Position position) {
      int rank = position.getRank();
      if (rank < 0 || rank >= TOP_RANKS.length) {
         return 0;
      }
      return (int) (TOP_RANKS[rank] * position.getMediaValue());
   }
}

Iterator

Iterator (behavioral group) has been used for years everewhere when developer needs to process data from collection, stream, file or from other sources. From Java 8 it is not more recommended way of doing data processing, powerful Stream API was introduced as a replacement. Stream processing model allows developers to focus on data processing operations instead of iteration logic. Code become more clear and easy to understand:

private static String capitalize(String sentence) {
   return Arrays.stream(sentence.split(" "))
      .filter(word -> word.length() > 4)
      .map(String::toUpperCase)
      .collect(Collectors.joining(" "));
}
 

Stream support was added in many parts of existing API like collections, files, different utilities. To make Stream API even more powerful and flexible libraries like StreamEx and jOOL was created in Java community. It is possible to convert your existing Iterator to Stream, but not always in very trivial way:

public class ModernIteratorClient {
   public static void main(String[] args) {
      ModernText text = new ModernMultilineText("This is just \n" +
         " a simple multiline \n" +
         "\n" +
         " text\n" +
         "\n", "\n");
      text.linesStream().forEach(System.out::println);
   }
}

public interface ModernText {
   Stream linesStream();
}

public class ModernMultilineText implements ModernText {
   private final String text;
   private final String lineSeparator;

   public ModernMultilineText(String text, String lineSeparator) {
      this.text = text;
      this.lineSeparator = lineSeparator;
   }

   @Override
   public Stream linesStream() {
      return StreamSupport.stream(new LineSupplier(), false);
   }

   private class LineSupplier extends Spliterators.AbstractSpliterator {
      private int lineStartIndex = 0;

      public LineSupplier() {
         super(Long.MAX_VALUE, Spliterator.NONNULL | Spliterator.SIZED);
      }

      @Override
      public boolean tryAdvance(Consumer<? super String> action) {
          if (lineStartIndex >= text.length()) {
             return false;
          }
          int separatorIndex = text.indexOf(lineSeparator, lineStartIndex + 1);
          if (separatorIndex < 0) {
             separatorIndex = text.length() - 1;
          }
          String line = text.substring(lineStartIndex, separatorIndex);
          action.accept(line);
          lineStartIndex = separatorIndex + 1;
          return true;
      }
   }
}

Summary

With time many problems changed or disappeared because of new language features, so classical design patterns should be adapted for this new reality. But Java is changing very slowly and we still have many typical problems in day to day job with this language. To avoid wasting time on inventing your own wheel every experienced Java developer must know design patterns and use them when required. This is important factor to improve development efficiency and produce more reliable solutions.

Full set of samples is available on GitHub, I also have a talk on this topic (slides in English on Slideshare and video in Russian from JavaDay Kiev conferences).

Обсуждение (
Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280

Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280

Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280

Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280

Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280

Warning: A non-numeric value encountered in /sata1/home/users/xpinjecti/www/www.xpinjection.com/wp-includes/pomo/plural-forms.php on line 280
0)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Мы используем файлы cookies для различных целей, включая аналитику и персонализированный маркетинг. Продолжая пользоваться сайтом, вы соглашаетесь на использование файлов cookies. Подробно ознакомиться с правилами работы с файлами cookies можно здесь

принять