Паттерны проектирования (design patterns) это типовые, проверенные практикой решения повторяющихся задач при проектировании кода: не готовый код, а описание подхода. Классический каталог делит их на три группы: порождающие (как создавать объекты), структурные (как связывать классы и объекты) и поведенческие (как объекты взаимодействуют). Почти каждый из них уже реализован внутри JDK: StringBuilder это Строитель, Calendar.getInstance() это Фабричный метод, Comparator в TreeSet это Стратегия. Паттерны или шаблоны проектирования: часть работы разработчика, которую часто недооценивают, что приводит к тому, что код становится трудно поддерживать и адаптировать под новые требования. Предлагаю посмотреть на то, что это вообще такое и как это используется в JDK. Естественно, все базовые шаблоны в том или ином виде уже давно вокруг нас. Давайте увидим их в рамках данного обзора.
Ножницы вырезают по пунктирным линиям разной формы: шаблон, по которому режут
Содержание:

Кратко

  • Паттерн это описание подхода к типовой задаче проектирования, а не алгоритм с конкретными шагами; знать их полезно, чтобы код переживал смену требований, и это частое требование в вакансиях.
  • Порождающие паттерны отвечают за создание объектов: Строитель (StringBuilder), Фабричный метод (Calendar.getInstance()), Абстрактная фабрика, Прототип (Cloneable), Одиночка (Runtime.getRuntime()).
  • Структурные паттерны отвечают за связи между классами: Заместитель (java.lang.reflect.Proxy), Декоратор (Collections.checkedList()), Адаптер (Arrays.asList()), Компоновщик (java.awt.Container), Мост (Component и ComponentPeer в AWT), Фасад, Легковес (кэш Integer.valueOf() и пул строк).
  • Поведенческие паттерны отвечают за взаимодействие объектов: Стратегия (Comparator), Состояние, Команда (Runnable), Цепочка обязанностей (обработчики java.util.logging), Итератор, Посетитель (FileVisitor), Наблюдатель (Observer), Посредник (Timer), Шаблонный метод (AbstractList), Снимок (Serializable).
  • Похожие паттерны различаются одним признаком: Декоратор оборачивает то, что ему передали, а Заместитель может сам управлять объектом; Стратегия меняет способ действия, Команда само действие; о Фасаде компоненты не знают, а Посредника знают и общаются только через него.
  • java.util.Observer и Observable с Java 9 помечены как устаревшие: вместо них слушатели из java.beans, структуры из java.util.concurrent или Flow API.
Надпись PATTERNS на фоне из шестиугольников: обложка статьи о паттернах проектирования

Шаблоны

Одно из часто встречаемых требований в вакансиях: "Знание паттернов". Прежде всего стоит ответить на простой вопрос: "А что такое Паттерн проектирования?". Паттерн переводится с английского как "шаблон". То есть это некоторый образец, по которому мы что-то делаем. Так и в программировании. Есть некоторые выработанные лучшие практики (best practice) и подходы к решению часто встречаемых проблем. Каждый программист это архитектор. Даже когда Вы создаете всего несколько классов или даже один, от Вас зависит, насколько код сможет выживать под изменением требований, насколько он удобен в использовании другими. И вот тут как раз и поможет знание шаблонов, т.к. это позволит быстрее понять, как лучше написать код так, чтобы не переписывать его. Как известно, программисты люди ленивые и проще написать сразу хорошо, чем переделывать несколько раз ) Еще паттерны могут показаться похожими на алгоритмы. Но у них есть разница. Алгоритм состоит из конкретных шагов, описывающих необходимые действия. Паттерны же лишь описывают подход, но не описывают шаги реализации. Паттерны бывают разные, т.к. решают разные проблемы. Обычно выделяют следующие категории:
  • Порождающие

    Эти паттерны решают проблемы обеспечения гибкости создания объектов

  • Структурные

    Эти паттерны решают проблемы эффективного построения связей между объектами

  • Поведенческие

    Эти паттерны решают проблемы эффективного взаимодействия между объектами

Для рассмотрения примеров предлагаю воспользоваться любым онлайн-компилятором Java или своей IDE.
Рисунок фабрики с дымящей трубой: порождающие паттерны отвечают за создание объектов

Порождающие паттерны (creational patterns)

Начнем с начала жизненного цикла объектов: с их создания. Порождающие шаблоны как раз и помогают создавать объекты удобнее, обеспечить гибкость этого процесса.

Строитель (Builder)

Одним из самых известных является "Строитель" (Builder). Данный паттерн позволяет создавать сложные объекты пошагово. В Java самый известный пример это 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());
  }
}

Фабричный метод (Factory Method)

Другой известный подход к созданию объекта: вынос создания в отдельный метод. Такой метод как бы становится фабрикой объектов. Поэтому и шаблон называется "Фабричный метод" (Factory Method). В Java, например, его действие можно увидеть на примере класса java.util.Calendar. Сам класс Calendar абстрактный, а чтобы его создавать используется метод 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());
  }
}
Часто это обусловлено тем, что логика создания объекта может быть непростой. Например, в случае выше, мы обращаемся к базовому классу Calendar, а создается класс GregorianCalendar. Если мы заглянем внутрь getInstance, то увидим, что в зависимости от локали создаются разные реализации Calendar. Для работы с датами сегодня используют java.time (появился в Java 8: Oracle называет Calendar устаревшим механизмом с изменяемыми объектами и странной нумерацией месяцев), но как иллюстрация фабричного метода Calendar.getInstance() остается наглядным.

Абстрактная фабрика (Abstract Factory)

Но иногда одного фабричного метода мало. Иногда требуется создавать разные объекты так, чтобы они друг с другом сочетались. В этом нам поможет другой шаблон: "Абстрактная фабрика" (Abstract factory). И тогда нам требуется создавать разные фабрики в одном месте. При этом плюсом является то, что нам не важны детали реализации, т.е. не важно, какую конкретно фабрику мы получим. Главное, чтобы она создавала правильные реализации. Супер пример:
UML-схема абстрактной фабрики: интерфейс GUIFactory с реализациями WinFactory и MacFactory создает кнопки и чекбоксы под свою операционную систему
То есть в зависимости от окружения (от операционной системы) мы получим определенную фабрику, которая создаст совместимые элементы.

Прототип (Prototype)

Как альтернатива подхода к созданию через кого-то, мы можем воспользоваться паттерном "Прототип" (Prototype). Суть его проста: новые объекты создаются по образу и подобию уже существующих объектов, т.е. по их прототипу. В Java с этим паттерном сталкивался каждый, это использование интерфейса 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
    }
  }
}
Как видно, вызывающий не знает, как устроен метод clone. То есть создание объекта по прототипу это обязанность самого объекта. Это полезно потому, что не завязывает использующего на реализацию объекта-шаблона.

Одиночка (Singleton)

Ну и самый последний в этом списке: паттерн "Одиночка" (Singleton). Цель его проста: обеспечить единственный экземпляр объекта на все приложение. В JDK самый простой пример это Runtime.getRuntime(): у каждого приложения ровно один объект Runtime, и создать второй нельзя. Данный паттерн интересен тем, что на нем часто показывают проблемы многопоточности. Для более глубокого ознакомления следует ознакомиться с этими статьями:
Черно-белая сеть из узлов и связей: структурные паттерны описывают связи между объектами

Структурные паттерны (structural patterns)

С созданием объектов стало понятнее. И теперь самое время посмотреть на структурные паттерны. Их цель: построение удобных в поддержке иерархий классов и их взаимосвязей.

Заместитель (Proxy)

Один из первых и всем известных паттернов: "Заместитель" (Proxy). Заместитель имеет тот же интерфейс, что и реальный объект, поэтому для клиента нет разницы, работать через заместителя или напрямую. Самым простым примером является 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"));
  }
}
Как видно, в примере у нас есть original, это HashMap, который реализует интерфейс Map. Мы далее создаем прокси, который замещает оригинальную HashMap для клиентской части, которая вызывает методы put и get, добавляя во время вызова свою логику.

Декоратор (Decorator)

Как мы видим, взаимодействие в паттерне идет через интерфейсы. Но иногда заместителя недостаточно. И тогда может быть использован паттерн "Декоратор" (Decorator). Декоратор еще называют оберткой или враппером (Wrapper). Прокси и декоратор очень похожи, но если посмотреть на пример, будет видна разница:

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);
  }
}
В отличие от прокси, декоратор оборачивается вокруг чего-то, что передали на вход. Прокси же может как принимать то, что нужно проксировать, так и сам управлять жизнью проксируемого объекта (например, создавать проксируемый объект).

Адаптер (Adapter)

Есть еще один интересный паттерн: "Адаптер" (Adapter). Он похож на декоратор: на вход декоратор принимает один объект и возвращает обертку над этим объектом. Отличие в том, что цель у этого не изменение функционала, а адаптация одного интерфейса к другому. В Java есть очень яркий пример на этот счет:

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));
  }
}
На входе у нас массив. Далее мы создаем адаптер, приводящий массив к интерфейсу List. Работая с ним мы на самом деле работаем с массивом. Поэтому, добавлять элементы не выйдет, т.к. массив изначальный не изменить. И мы в этом случае получим UnsupportedOperationException.

Компоновщик (Composite)

Следующим интересным подходом в разработке структуры классов является паттерн "Компоновщик" (Composite). Интересен он тем, что некоторый набор элементов использующих один интерфейс выстраиваются в некоторую древовидную иерархию. Вызывая метод в родительском элементе мы получаем вызов этого метода по всем необходимым дочерним элементам. Яркий пример этого паттерна: UI (будь то java.awt или 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());
  }
}
Как мы видим, мы добавили в контейнер компонент. А потом попросили контейнер применить новую ориентацию компонентов. И контейнер, зная из каких компонентов он состоит, делегировал выполнение этой команды всем дочерним компонентам.

Мост (Bridge)

Еще одним из интересных паттернов является паттерн "Мост" (Bridge). Называется он так, потому что описывает соединение или мост между двумя различными иерархиями классов. Одну из этих иерархий считают абстракцией, а другую реализацией. Так выделено потому что абстракция сама не выполняет действия, а делегирует это выполнение реализации. Такой паттерн часто применяют тогда, когда есть классы "управления" и несколько видов классов "платформ" (например, Windows, Linux и т.д.). При таком подходе одна из этих иерархий (абстракция) получит ссылку на объекты другой иерархии (реализация) и будет делегировать им основную работу. Благодаря тому, что все реализации будут следовать общему интерфейсу, их можно будет взаимозаменять внутри абстракции. В Java яркий пример этому java.awt:
UML-схема моста в AWT: иерархия абстракций Component и Button отдельно от иерархии реализаций ComponentPeer и ButtonPeer для Win NT и Motif
Здесь Button это абстракция, с которой работает программист, а ButtonPeer под конкретную платформу это реализация, которой абстракция делегирует отрисовку.

Фасад (Facade)

Среди структурных паттернов также хочется отметить паттерн "Фасад" (Facade). Суть его в том, чтобы за удобным и лаконичным интерфейсом спрятать сложность использования библиотек/фреймворков, стоящих за этим API. Например, можно привести JSF или EntityManager из JPA.

Легковес (Flyweight)

Также есть другой паттерн, называемый "Легковес" (Flyweight). Его суть заключается в том, что если у разных объектов есть одинаковое состояние, то его можно обобщить и хранить не в каждом объекте, а в одном месте. И тогда каждый объект сможет ссылаться на общую часть, что позволит сократить расходы памяти на хранение. Часто работа данного паттерна связана с предварительным кэшированием или с поддержанием пула объектов. Интересно, что этот паттерн мы тоже знаем с самого начала:
Исходный код Integer.valueOf(int) с комментарием о кэшировании значений от -128 до 127
По той же аналогии сюда можно отнести пул строк: одинаковые строковые литералы хранятся в одном экземпляре.
Слово BEHAVIOR на фоне из иконок: поведенческие паттерны

Поведенческие шаблоны

Итак, мы разобрались, как можно создать объекты и как можно организовать связь между классами. Осталось самое интересное: обеспечить гибкость в изменении поведения объектов. И в этом нам помогут поведенческие паттерны.

Стратегия (Strategy)

Одним из самых часто упоминаемых паттернов является паттерн "Стратегия" (Strategy). С него же начинается изучение паттернов в книге "Head First. Паттерны проектирования". При помощи паттерна "Стратегия" мы можем внутри объекта хранить то, каким образом мы будем выполнять действие, т.е. объект внутри хранит стратегию, которая может быть изменена в том числе во время выполнения кода. Этот паттерн мы часто используем, когда применяем компаратор:

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<String> dataSet = new TreeSet<>(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Перед нами TreeSet. У него есть поведение: TreeSet поддерживает порядок элементов, т.е. сортирует их (т.к. он является SortedSet). У этого поведения есть стратегия, определенная по умолчанию, которую мы видим в JavaDoc: сортировка в "natural ordering" (для строк это лексикографический порядок). Так происходит, если использовать конструктор без параметров. Но если мы захотим поменять стратегию, то мы можем передать в конструктор Comparator. В данном примере мы можем создать наш набор как new TreeSet(comparator), и тогда порядок хранения элементов (стратегия хранения) поменяется на тот, который указан в компараторе.

Состояние (State)

Интересно, что есть почти такой же паттерн с названием "Состояние" (State). Паттерн "Состояние" говорит, что если у нас есть у главного объекта некоторое поведение, зависимое от состояния этого объекта, то тогда можно описать само состояние в виде объекта и менять объект состояния. А вызовы из главного объекта делегировать состоянию.

Команда (Command)

Еще один паттерн, известный нам с изучения самых основ языка Java: паттерн "Команда" (Command). Этот паттерн проектирования говорит о том, что различные команды можно представлять в виде разных классов. Данный паттерн очень похож на паттерн "Стратегия". Но в паттерне "Стратегия" мы переопределяли то, как будет выполняться конкретное действие (например, сортировка в TreeSet). В паттерне "Команда" же мы переопределяем то, какое вообще действие будет выполнено. Паттерн команда с нами каждый день, когда мы используем потоки:

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();
  }
}
Как видим, command определяет действие или команду, которая будет выполнена в новом потоке.

Цепочка обязанностей (Chain of Responsibility)

Также стоит рассмотреть и паттерн "Цепочка обязанностей" (Chain of Responsibility). Данный паттерн тоже очень прост. Этот паттерн говорит, что если что-то надо обработать, то можно собрать обработчики в цепочку. Например, такой шаблон часто используется в веб-серверах. На входе сервер имеет некоторый запрос от пользователя. Дальше этот запрос проходит цепочку обработки. В этой цепочке обработчиков есть фильтры (например, не принимать запросы из черного списка IP-адресов), обработчики аутентификации (пускать только разрешенных пользователей), обработчик заголовков запроса, обработчик кэширования и т.д. Но есть в 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");
  }
}
Как видно, обработчики (Handlers) добавляются в список обработчиков логгера. Когда логгер получает сообщение для обработки, каждое такое сообщение проходит через цепочку хэндлеров (из logger.getHandlers) данного логгера.

Итератор (Iterator)

Еще один паттерн, который мы видим каждый день: "Итератор" (Iterator). Суть его заключается в том, чтобы разделить коллекцию объектов (т.е. класс, представляющий структуру данных. Например, List) и обход этой коллекции.

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());
    }
  }
}
Как видно, итератор не является частью коллекции, а представлен отдельным классом, который обходит коллекцию. Использующий итератор даже может не знать про то, по какой коллекции он итерируется, т.е. какую коллекцию он обходит.

Посетитель (Visitor)

Стоит рассмотреть и паттерн "Посетитель" (Visitor). Паттерн посетитель очень похож на итератор. Данный паттерн помогает обходить структуру объектов и выполнять действия над этими объектами. Отличаются они скорее концепцией. Итератор обходит коллекцию так, что клиенту, использующему итератор, все равно, что за коллекция внутри, важны лишь элементы из последовательности. Посетитель же именно про то, что есть некоторая иерархия или структура объектов, которые мы посещаем. Например, мы можем использовать отдельную обработку каталогов и отдельную обработку файлов. В Java "из коробки" есть реализация этого паттерна в виде 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<Path> visitor = new SimpleFileVisitor<>() {
      @Override
      public FileVisitResult visitFile(Path 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
    }
  }
}

Наблюдатель (Observer)

Иногда возникает необходимость одним объектам реагировать на изменения в других объектах, и тогда нам поможет паттерн "Наблюдатель" (Observer). Самый удобный способ это обеспечить механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. Данный паттерн часто применяется в различных Listener'ах и Observer'ах, реагирующих на разные события. Как самый простой пример можно вспомнить реализацию этого паттерна из 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!");
  }
}
Этот пример сегодня чисто исторический: с Java 9 Observer и Observable помечены как @Deprecated. Документация объясняет почему: модель событий у них слишком бедная, порядок уведомлений не гарантирован, а изменения состояния не соответствуют уведомлениям один к одному. Вместо них рекомендуют слушатели из пакета java.beans (например, PropertyChangeListener), структуры из java.util.concurrent или Flow API для реактивных потоков.

Посредник (Mediator)

Есть еще один полезный поведенческий шаблон: "Посредник" (Mediator). Полезен он тем, что в сложных системах помогает убрать связь между разными объектами и делегировать все взаимодействия между объектами некоторому объекту, который и является посредником. Одним из самых ярких применений данного паттерна является Spring MVC: его DispatcherServlet принимает запрос и сам решает, какому контроллеру, view resolver и обработчику исключений его передать, так что эти компоненты друг о друге не знают. Часто можно увидеть в примерах также 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);
  }
}
Пример внешне скорее напоминает паттерн команда. А суть паттерна "Посредник" скрыта в реализации Timer'а. Внутри таймера есть очередь задач TaskQueue, есть поток TimerThread. Мы, как клиенты этого класса, не взаимодействуем с ними, а взаимодействуем с Timer'ом, который на наш вызов его методов обращается к методам других объектов, посредником которых является. Внешне может показаться очень похоже на "Фасад". Но разница в том, что когда используется Фасад, компоненты не знают, что фасад существует и обращаются друг к другу. А когда используется "Посредник", то компоненты знают и используют посредника, но не обращаются друг к другу напрямую.

Шаблонный метод (Template Method)

Стоит рассмотреть и паттерн "Шаблонный метод" (Template Method). Понятный уже по названию шаблон. Суть заключается в том, что код написан так, что пользователям кода (разработчикам) предоставляется некоторый шаблон алгоритма, шаги в котором разрешается переопределять. Это позволяет пользователям кода не писать весь алгоритм, а думать только над тем, как правильно выполнить тот или иной шаг этого алгоритма. Например, в Java есть абстрактный класс AbstractList, определяющий поведение итератора по List. Однако, сам итератор использует методы листа, такие как: get, set, remove. Поведение этих методов определяет разработчик наследников AbstractList. Таким образом итератор в AbstractList является шаблоном алгоритма итерирования по листу. А разработчики конкретных реализаций AbstractList меняют поведение этого итерирования, определяя поведение конкретных шагов.

Снимок (Memento)

Последний из разбираемых нами паттернов: паттерн "Снимок" (Memento). Суть его заключается в сохранении некоторого состояния объекта с возможностью это состояние восстановить. Самым узнаваемым примером из JDK является сериализация объекта, т.е. java.io.Serializable. Давайте рассмотрим пример:

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 for saved state
    }
  }
}
Рисунок от руки: стрелки от нескольких кружков сходятся к одному, подпись Заключение

Заключение

Как мы увидели из обзора, паттернов существует огромное множество. Каждый из них решает свою задачу. И знание этих паттернов может помочь Вам вовремя понять, как написать Вашу систему так, чтобы она была гибкая, поддерживаемая и устойчивая к изменениям. Сведем все примеры из статьи в одну таблицу:
Категория Паттерн Пример из статьи
Порождающие Строитель (Builder) StringBuilder
Порождающие Фабричный метод (Factory Method) Calendar.getInstance()
Порождающие Абстрактная фабрика (Abstract Factory) фабрики UI-элементов под разные ОС (схема выше)
Порождающие Прототип (Prototype) Cloneable и clone()
Порождающие Одиночка (Singleton) Runtime.getRuntime()
Структурные Заместитель (Proxy) java.lang.reflect.Proxy
Структурные Декоратор (Decorator) Collections.checkedList()
Структурные Адаптер (Adapter) Arrays.asList()
Структурные Компоновщик (Composite) java.awt.Container
Структурные Мост (Bridge) Component и ComponentPeer в java.awt
Структурные Фасад (Facade) JSF, EntityManager из JPA
Структурные Легковес (Flyweight) кэш Integer.valueOf(), пул строк
Поведенческие Стратегия (Strategy) Comparator в TreeSet
Поведенческие Состояние (State) объект-состояние внутри главного объекта (без примера из JDK)
Поведенческие Команда (Command) Runnable и Thread
Поведенческие Цепочка обязанностей (Chain of Responsibility) Handler в java.util.logging
Поведенческие Итератор (Iterator) Iterator
Поведенческие Посетитель (Visitor) FileVisitor и Files.walkFileTree()
Поведенческие Наблюдатель (Observer) Observer и Observable (устарели с Java 9)
Поведенческие Посредник (Mediator) java.util.Timer
Поведенческие Шаблонный метод (Template Method) AbstractList
Поведенческие Снимок (Memento) Serializable и ObjectOutputStream

Вопросы и ответы

Что такое паттерн проектирования простыми словами?

Это проверенное практикой описание того, как решать типовую задачу проектирования: как создать объект, как связать классы, как организовать их взаимодействие. В отличие от алгоритма паттерн не задает конкретные шаги, а только подход, поэтому один и тот же паттерн в разных проектах выглядит по-разному. Знание паттернов дает общий язык с другими разработчиками и помогает писать код, который переживет смену требований.

Какие бывают категории паттернов проектирования?

Три. Порождающие отвечают за создание объектов (Строитель, Фабричный метод, Абстрактная фабрика, Прототип, Одиночка). Структурные отвечают за связи между классами и объектами (Заместитель, Декоратор, Адаптер, Компоновщик, Мост, Фасад, Легковес). Поведенческие отвечают за взаимодействие объектов (Стратегия, Состояние, Команда, Цепочка обязанностей, Итератор, Посетитель, Наблюдатель, Посредник, Шаблонный метод, Снимок).

Чем Заместитель (Proxy) отличается от Декоратора (Decorator)?

Оба имеют тот же интерфейс, что и оборачиваемый объект, разница в ответственности. Декоратор всегда получает готовый объект на вход и добавляет ему поведение: Collections.checkedList(list, String.class) оборачивает переданный список и проверяет тип добавляемых элементов. Заместитель контролирует доступ к объекту и может сам управлять его жизнью, вплоть до создания: java.lang.reflect.Proxy перехватывает каждый вызов метода через InvocationHandler.

Чем Стратегия отличается от Команды?

Стратегия меняет то, как выполняется одно и то же действие: TreeSet всегда сортирует, а Comparator задает, по какому правилу. Команда меняет само действие: Runnable описывает, что именно выполнить, а Thread лишь запускает его. Внешне оба выглядят как объект, переданный в конструктор.

Чем Фасад отличается от Посредника?

Фасад прячет сложную подсистему за простым интерфейсом, при этом компоненты подсистемы о фасаде не знают и общаются друг с другом напрямую. Посредник, наоборот, известен всем участникам: они обращаются только к нему и не знают друг о друге. java.util.Timer скрывает очередь задач и поток TimerThread, с которыми клиент никогда не работает напрямую.

Почему Observer и Observable в Java устарели и чем их заменить?

С Java 9 java.util.Observer и Observable помечены как @Deprecated: у них бедная модель событий, порядок уведомлений не гарантирован, а изменение состояния и уведомление не связаны один к одному. Документация Oracle предлагает три замены: слушатели из java.beans (PropertyChangeListener), структуры из java.util.concurrent для обмена между потоками и Flow API (java.util.concurrent.Flow) для реактивных потоков. Сам паттерн Наблюдатель при этом никуда не делся: любой Listener в Swing или Android это он же.

Читайте также

#Viacheslav