«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
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);
}
}
Фильтры сервлетов — это пример из экосистемы JEE, который работает таким образом. Один экземпляр получает запрос и ответ сервлета, а экземпляр FilterChain представляет всю цепочку фильтров. Затем каждый из них должен выполнить свою работу, а затем либо завершить цепочку, либо вызвать функцию chain.doFilter(), чтобы передать управление следующему фильтру:
3. Команда
Шаблон Command позволяет нам инкапсулировать некоторые конкретные действия — или команды — за общим интерфейсом, чтобы их можно было правильно активировать во время выполнения.
interface DoorCommand {
perform(Door door);
}
class OpenDoorCommand implements DoorCommand {
public void perform(Door door) {
door.setState("open");
}
}
Обычно у нас есть интерфейс Command, экземпляр Receiver, который получает экземпляр команды, и Invoker, который отвечает за вызов правильного экземпляра команды. Затем мы можем определить различные экземпляры нашего командного интерфейса для выполнения различных действий с получателем:
Здесь у нас есть реализация команды, которая будет принимать дверь в качестве получателя и заставит дверь стать â «открыть». Наш вызывающий может затем вызвать эту команду, когда он хочет открыть данную дверь, и команда инкапсулирует, как это сделать.
В будущем нам может понадобиться изменить команду OpenDoorCommand, чтобы сначала проверить, не заперта ли дверь. Это изменение будет полностью внутри команды, и классы получателя и вызывающего не должны иметь никаких изменений.
Action saveAction = new SaveAction();
button = new JButton(saveAction)
3.1. Примеры в JVM
Очень распространенным примером этого шаблона является класс Action в Swing:
Здесь SaveAction — это команда, компонент Swing JButton, который использует этот класс, является инициатором, а реализация Action вызывается с ActionEvent в качестве получателя.
void printAll<T>(Iterator<T> iter) {
while (iter.hasNext()) {
System.out.println(iter.next());
}
}
4. Итератор
Шаблон Iterator позволяет нам работать с элементами в коллекции и взаимодействовать с каждым по очереди. Мы используем это для написания функций, использующих произвольный итератор для некоторых элементов, независимо от того, откуда они берутся. Источником может быть упорядоченный список, неупорядоченный набор или бесконечный поток:
4.1. Примеры в JVM
Все стандартные коллекции JVM реализуют шаблон Iterator, предоставляя метод iterator(), который возвращает Iterator\u003cT\u003e для элементов коллекции. Потоки также реализуют тот же метод, за исключением того, что в этом случае это может быть бесконечный поток, поэтому итератор никогда не завершится.
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 позволяет нам создавать объекты, способные изменять состояние, а затем возвращаться в прежнее состояние. По сути, это функция «отмены» для состояния объекта.
Это можно реализовать относительно легко, сохраняя предыдущее состояние всякий раз, когда вызывается сеттер:
Это дает возможность отменить последнее изменение, внесенное в объект.
Это часто реализуется путем помещения всего состояния объекта в один объект, известный как Memento. Это позволяет сохранять и восстанавливать все состояние за одно действие, вместо того, чтобы сохранять каждое поле по отдельности.
5.1. Примеры в JVM
«JavaServer Faces предоставляет интерфейс StateHolder, который позволяет разработчикам сохранять и восстанавливать свое состояние. Это реализуется несколькими стандартными компонентами, состоящими из отдельных компонентов, например, HtmlInputFile, HtmlInputText или HtmlSelectManyCheckbox, а также составных компонентов, таких как HtmlForm.
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. Наблюдатель
Шаблон Наблюдатель позволяет объекту указывать другим, что произошли изменения. Обычно у нас есть субъект — объект, излучающий события, и ряд наблюдателей — объекты, получающие эти события. Наблюдатели зарегистрируют у субъекта, что они хотят получать информацию об изменениях. Как только это произойдет, любые изменения, которые происходят в субъекте, будут информировать наблюдателей:
Это берет набор прослушивателей событий и вызывает каждый из них каждый раз, когда состояние изменяется с новым значением состояния.
6.1. Примеры в JVM
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 имеет стандартную пару классов, которые позволяют нам делать именно это — java.beans.PropertyChangeSupport и java.beans.PropertyChangeListener.
PropertyChangeSupport действует как класс, в который можно добавлять и удалять наблюдателей, а также уведомлять их обо всех изменениях состояния. PropertyChangeListener — это интерфейс, который наш код может реализовать для получения любых произошедших изменений:
Обратите внимание, что есть еще пара классов, которые кажутся более подходящими — java.util.Observer и java.util. Наблюдаемый. Однако в 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() и других методов, являются подключаемыми стратегиями, предоставляемыми универсальному методу.
// Sort by name
Collections.sort(users, new UsersNameComparator());
// Sort by ID
Collections.sort(users, new UsersIdComparator());
Однако примеры восходят еще дальше. Интерфейс Comparator, представленный в Java 1.2, представляет собой стратегию, которую можно использовать для сортировки элементов в коллекции по мере необходимости. Мы можем предоставить разные экземпляры компаратора для сортировки одного и того же списка разными способами:
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. Посетитель
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);
}
}
Шаблон Посетитель позволяет нашему коду обрабатывать различные подклассы типобезопасным способом, не прибегая к проверкам instanceof. У нас будет интерфейс посетителя с одним методом для каждого конкретного подкласса, который нам нужно поддерживать. Тогда наш базовый класс будет иметь метод accept(Visitor). Каждый из подклассов будет вызывать соответствующий метод для этого посетителя, передавая себя. Это позволяет нам реализовать конкретное поведение в каждом из этих методов, каждый из которых знает, что он будет работать с конкретным типом:
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;
}
}
~~ ~ Здесь у нас есть интерфейс UserVisitor с тремя различными методами посетителя. В нашем примере StandardUser вызывает соответствующий метод, и то же самое будет сделано в AdminUser и Superuser. Затем мы можем написать нашим посетителям, чтобы они работали с ними по мере необходимости:
Наш стандартный пользователь никогда не имеет разрешения, наш суперпользователь всегда имеет разрешение, и наш администратор может иметь разрешение, но это нужно искать в самом пользователе.
9.1. Примеры в JVM
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);
}
});
Среда Java NIO2 использует этот шаблон с Files.walkFileTree(). Для этого требуется реализация FileVisitor, которая имеет методы для обработки различных аспектов обхода дерева файлов. Затем наш код может использовать это для поиска файлов, распечатки соответствующих файлов, обработки большого количества файлов в каталоге или множества других вещей, которые должны работать в каталоге:
10. Заключение