Перейти к основному содержимому

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

· 10 мин. чтения

1. Введение

Недавно мы рассмотрели Creational Design Patterns и где их найти в JVM и других основных библиотеках. Теперь мы рассмотрим шаблоны поведенческого проектирования . Они сосредоточены на том, как наши объекты взаимодействуют друг с другом или как мы взаимодействуем с ними.

2. Цепочка ответственности

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

interface ChainOfResponsibility {
void perform();
}
class LoggingChain {
private ChainOfResponsibility delegate;

public void perform() {
System.out.println("Starting chain");
delegate.perform();
System.out.println("Ending chain");
}
}

Здесь мы видим пример, в котором наша реализация выводит данные до и после вызова делегата.

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

2.1. Примеры в JVM

Фильтры сервлетов — это пример из экосистемы JEE, который работает таким образом. Один экземпляр получает запрос и ответ сервлета, а экземпляр FilterChain представляет всю цепочку фильтров. Затем каждый из них должен выполнить свою работу, а затем либо завершить цепочку, либо вызвать chain.doFilter() , чтобы передать управление следующему фильтру :

public class AuthenticatingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (!"MyAuthToken".equals(httpRequest.getHeader("X-Auth-Token")) {
return;
}
chain.doFilter(request, response);
}
}

3. Команда

Шаблон Command позволяет нам инкапсулировать некоторые конкретные действия — или команды — за общим интерфейсом, чтобы их можно было правильно запускать во время выполнения.

Обычно у нас есть интерфейс Command, экземпляр Receiver, который получает экземпляр команды, и Invoker, который отвечает за вызов правильного экземпляра команды. Затем мы можем определить разные экземпляры нашего командного интерфейса для выполнения различных действий с приемником :

interface DoorCommand {
perform(Door door);
}
class OpenDoorCommand implements DoorCommand {
public void perform(Door door) {
door.setState("open");
}
}

Здесь у нас есть реализация команды, которая примет дверь в качестве получателя и заставит дверь стать «открытой». Наш вызывающий может затем вызвать эту команду, когда он хочет открыть данную дверь, и команда инкапсулирует, как это сделать.

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

3.1. Примеры в JVM

Очень распространенным примером этого шаблона является класс Action в Swing:

Action saveAction = new SaveAction();
button = new JButton(saveAction)

Здесь SaveAction — это команда, компонент Swing JButton , который использует этот класс, является инициатором, а реализация Action вызывается с ActionEvent в качестве получателя.

4. Итератор

Шаблон Iterator позволяет нам работать с элементами в коллекции и взаимодействовать с каждым по очереди. Мы используем это для написания функций, использующих произвольный итератор для некоторых элементов, независимо от того, откуда они берутся . Источником может быть упорядоченный список, неупорядоченный набор или бесконечный поток:

void printAll<T>(Iterator<T> iter) {
while (iter.hasNext()) {
System.out.println(iter.next());
}
}

4.1. Примеры в JVM

Все стандартные коллекции JVM реализуют шаблон Iterator , предоставляя метод iterator() , который возвращает Iterator<T> для элементов коллекции. Потоки также реализуют тот же метод, за исключением того, что в этом случае это может быть бесконечный поток, поэтому итератор никогда не завершится.

5. Сувениры

Паттерн Memento позволяет нам создавать объекты, способные изменять состояние, а затем возвращаться в прежнее состояние. По сути, это функция «отмены» для состояния объекта.

Это можно реализовать относительно легко, сохраняя предыдущее состояние каждый раз, когда вызывается установщик:

class Undoable {
private String value;
private String previous;

public void setValue(String newValue) {
this.previous = this.value;
this.value = newValue;
}

public void restoreState() {
if (this.previous != null) {
this.value = this.previous;
this.previous = null;
}
}
}

Это дает возможность отменить последнее изменение, внесенное в объект.

Это часто реализуется путем помещения всего состояния объекта в один объект, известный как Memento. Это позволяет сохранять и восстанавливать все состояние за одно действие, вместо того, чтобы сохранять каждое поле по отдельности.

5.1. Примеры в JVM

JavaServer Faces предоставляет интерфейс StateHolder , который позволяет разработчикам сохранять и восстанавливать свое состояние . Существует несколько стандартных компонентов, реализующих это, состоящих из отдельных компонентов, например, HtmlInputFile , HtmlInputText или HtmlSelectManyCheckbox , а также составных компонентов, таких как HtmlForm .

6. Наблюдатель

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

class Observable {
private String state;
private Set<Consumer<String>> listeners = new HashSet<>;

public void addListener(Consumer<String> listener) {
this.listeners.add(listener);
}

public void setState(String newState) {
this.state = state;
for (Consumer<String> listener : listeners) {
listener.accept(newState);
}
}
}

Это берет набор прослушивателей событий и вызывает каждый из них каждый раз, когда состояние изменяется с новым значением состояния.

6.1. Примеры в JVM

В Java есть стандартная пара классов, которые позволяют нам делать именно это — java.beans.PropertyChangeSupport и java.beans.PropertyChangeListener .

PropertyChangeSupport действует как класс, в который можно добавлять и удалять наблюдателей, а также уведомлять их обо всех изменениях состояния. Затем PropertyChangeListener — это интерфейс, который наш код может реализовать для получения любых произошедших изменений:

PropertyChangeSupport observable = new PropertyChangeSupport();

// Add some observers to be notified when the value changes
observable.addPropertyChangeListener(evt -> System.out.println("Value changed: " + evt));

// Indicate that the value has changed and notify observers of the new value
observable.firePropertyChange("field", "old value", "new value");

Обратите внимание, что есть еще пара классов, которые кажутся более подходящими — java.util.Observer и java.util.Observable . Однако в Java 9 они устарели из-за негибкости и ненадежности.

7. Стратегия

Паттерн « Стратегия» позволяет нам писать общий код, а затем вставлять в него конкретные стратегии, чтобы получить конкретное поведение, необходимое для конкретных случаев.

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

interface NotificationStrategy {
void notify(User user, Message message);
}
class EmailNotificationStrategy implements NotificationStrategy {
....
}
class SMSNotificationStrategy implements NotificationStrategy {
....
}

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

7.1. Примеры в JVM

Стандартные библиотеки Java широко используют этот шаблон, часто таким образом, который на первый взгляд может показаться неочевидным . Например, Streams API , представленный в Java 8, широко использует этот шаблон. Лямбды, предоставленные для map() , filter() и других методов, являются подключаемыми стратегиями, которые предоставляются универсальному методу.

Однако примеры восходят еще дальше. Интерфейс Comparator , представленный в Java 1.2, представляет собой стратегию, которую можно использовать для сортировки элементов в коллекции по мере необходимости. Мы можем предоставить разные экземпляры Comparator для сортировки одного и того же списка по-разному:

// Sort by name
Collections.sort(users, new UsersNameComparator());

// Sort by ID
Collections.sort(users, new UsersIdComparator());

8. Шаблонный метод

Шаблон метода шаблона используется, когда мы хотим организовать несколько различных методов, работающих вместе. Мы определим базовый класс с методом шаблона и набором из одного или нескольких абстрактных методов — либо нереализованных, либо реализованных с некоторым поведением по умолчанию. Затем метод шаблона вызывает эти абстрактные методы по фиксированному шаблону. Затем наш код реализует подкласс этого класса и реализует эти абстрактные методы по мере необходимости:

class Component {
public void render() {
doRender();
addEventListeners();
syncData();
}

protected abstract void doRender();

protected void addEventListeners() {}

protected void syncData() {}
}

Здесь у нас есть несколько произвольных компонентов пользовательского интерфейса. Наши подклассы будут реализовывать метод doRender() для фактического рендеринга компонента. Мы также можем опционально реализовать методы addEventListeners() и syncData() . Когда наш UI-фреймворк визуализирует этот компонент, он гарантирует, что все три будут вызываться в правильном порядке.

8.1. Примеры в JVM

AbstractList , AbstractSet и AbstractMap , используемые коллекциями Java, имеют много примеров этого шаблона . Например, оба метода indexOf() и lastIndexOf() работают в терминах метода listIterator() , который имеет реализацию по умолчанию, но переопределяется в некоторых подклассах. В равной степени оба метода add(T) и addAll(int, T) работают с точки зрения метода add(int, T) , который не имеет реализации по умолчанию и должен быть реализован подклассом.

Java IO также использует этот шаблон в InputStream , OutputStream , Reader и Writer . Например, класс InputStream имеет несколько методов, которые работают с точки зрения read(byte[], int, int) , для реализации которых требуется подкласс.

9. Посетитель

Шаблон Visitor позволяет нашему коду обрабатывать различные подклассы безопасным способом, не прибегая к проверкам instanceof . У нас будет интерфейс посетителя с одним методом для каждого конкретного подкласса, который нам нужно поддерживать. Тогда наш базовый класс будет иметь метод accept(Visitor) . Каждый из подклассов будет вызывать соответствующий метод для этого посетителя, передавая себя. Затем это позволяет нам реализовать конкретное поведение в каждом из этих методов, каждый из которых знает, что он будет работать с конкретным типом:

interface UserVisitor<T> {
T visitStandardUser(StandardUser user);
T visitAdminUser(AdminUser user);
T visitSuperuser(Superuser user);
}
class StandardUser {
public <T> T accept(UserVisitor<T> visitor) {
return visitor.visitStandardUser(this);
}
}

Здесь у нас есть интерфейс UserVisitor с тремя различными методами посетителя. В нашем примере StandardUser вызывает соответствующий метод, и то же самое будет сделано в AdminUser и Superuser . Затем мы можем написать нашим посетителям, чтобы они работали с ними по мере необходимости:

class AuthenticatingVisitor {
public Boolean visitStandardUser(StandardUser user) {
return false;
}
public Boolean visitAdminUser(AdminUser user) {
return user.hasPermission("write");
}
public Boolean visitSuperuser(Superuser user) {
return true;
}
}

Наш StandardUser никогда не имеет разрешения, наш Superuser всегда имеет разрешение, и наш AdminUser может иметь разрешение, но это нужно искать в самом пользователе.

9.1. Примеры в JVM

Платформа Java NIO2 использует этот шаблон с Files.walkFileTree() . Для этого требуется реализация FileVisitor , которая имеет методы для обработки различных аспектов обхода дерева файлов. Затем наш код может использовать это для поиска файлов, распечатки соответствующих файлов, обработки множества файлов в каталоге или множества других вещей, которые должны работать в каталоге :

Files.walkFileTree(startingDir, new SimpleFileVisitor() {
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
System.out.println("Found file: " + file);
}

public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Found directory: " + dir);
}
});

10. Заключение

В этой статье мы рассмотрели различные шаблоны проектирования, используемые для поведения объектов. Мы также рассмотрели примеры использования этих шаблонов в основной JVM, поэтому мы можем увидеть их использование таким образом, который уже приносит пользу многим приложениям.