1. введение
Недавно мы рассмотрели Шаблоны креативного дизайна и где их найти в 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 представляет всю цепочку фильтров. Каждый из них должен затем выполнить свою работу, а затем либо завершить цепочку, либо вызвать цепочку.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. Команда
Шаблон команд позволяет нам инкапсулировать некоторые конкретные действия – или команды – за общим интерфейсом, чтобы они могли правильно запускаться во время выполнения.
Как правило, у нас будет командный интерфейс, экземпляр получателя, который получает экземпляр команды, и Вызыватель, который отвечает за вызов правильного экземпляра команды. Затем мы можем определить различные экземпляры нашего командного интерфейса для выполнения различных действий с получателем :
interface DoorCommand { perform(Door door); }
class OpenDoorCommand implements DoorCommand { public void perform(Door door) { door.setState("open"); } }
Здесь у нас есть реализация команды, которая примет Дверь в качестве приемника и приведет к тому, что дверь станет “открытой”. Затем наш вызывающий может вызвать эту команду, когда он хочет открыть данную дверь, и команда инкапсулирует, как это сделать.
В будущем нам, возможно, потребуется изменить нашу команду Открыть дверь , чтобы сначала убедиться, что дверь не заперта. Это изменение будет полностью внесено в команду, и классы получателя и вызывающего не нуждаются в каких-либо изменениях.
3.1. Примеры в JVM
Очень распространенным примером этого шаблона является класс Action в Swing:
Action saveAction = new SaveAction(); button = new JButton(saveAction)
Здесь Сохранить действие является командой, компонент Swing JButton , использующий этот класс, является вызывающим, а реализация Действие вызывается с ActionEvent в качестве получателя.
4. Итератор
Шаблон итератора позволяет нам работать с элементами коллекции и взаимодействовать с каждым по очереди. Мы используем это для написания функций, выполняющих произвольный итератор над некоторыми элементами, независимо от того, откуда они берутся . Источником может быть упорядоченный список, неупорядоченный набор или бесконечный поток:
void printAll(Iterator iter) { while (iter.hasNext()) { System.out.println(iter.next()); } }
4.1. Примеры в JVM
Все стандартные коллекции JVM реализуют шаблон Итератора , предоставляя итератор() метод , который возвращает Итератор по элементам в коллекции. Потоки также реализуют тот же метод, за исключением того, что в этом случае это может быть бесконечный поток, поэтому итератор может никогда не завершиться.
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; } } }
Это дает возможность отменить последнее изменение, внесенное в объект.
Это часто реализуется путем обертывания всего состояния объекта в один объект, известный как памятка. Это позволяет сохранять и восстанавливать все состояние за одно действие, вместо того, чтобы сохранять каждое поле по отдельности.
5.1. Примеры в JVM
JavaServer Faces предоставляет интерфейс под названием Держатель состояния , который позволяет разработчикам сохранять и восстанавливать свое состояние . Существует несколько стандартных компонентов, которые реализуют это, состоящих из отдельных компонентов – например, HtmlInputFile , HtmlInputText или HtmlSelectManyCheckbox – а также составных компонентов, таких как HtmlForm .
6. Наблюдатель
Шаблон Наблюдатель позволяет объекту указывать другим, что произошли изменения. Обычно у нас будет Субъект – объект, излучающий события, и ряд Наблюдателей – объекты, получающие эти события. Наблюдатели зарегистрируются у субъекта, которого они хотят проинформировать об изменениях. Как только это произойдет, любые изменения, которые произойдут в предмете, заставят наблюдателей быть проинформированными :
class Observable { private String state; private Set> listeners = new HashSet<>; public void addListener(Consumer listener) { this.listeners.add(listener); } public void setState(String newState) { this.state = state; for (Consumer listener : listeners) { listener.accept(newState); } } }
Это требует набора прослушивателей событий и вызывает каждый из них каждый раз, когда состояние изменяется с новым значением состояния.
6.1. Примеры в JVM
В Java есть стандартная пара классов, которые позволяют нам делать именно это – В Java есть стандартная пара классов, которые позволяют нам делать именно это – и и .
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.Наблюдатель и 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 широко используют этот шаблон, часто способами, которые на первый взгляд могут показаться неочевидными|/. Например, API Streams , представленный в Java 8, широко использует этот шаблон. Лямбды, предоставляемые map () , filter () и другими методами, являются подключаемыми стратегиями, предоставляемыми для универсального метода.
Однако примеры уходят еще дальше. Интерфейс Comparator , представленный в Java 1.2, представляет собой стратегию, которая может быть предоставлена для сортировки элементов в коллекции по мере необходимости. Мы можем предоставить различные экземпляры Компаратора для сортировки одного и того же списка различными способами по желанию:
// 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() для фактического отображения компонента. Мы также можем дополнительно реализовать методы addeventlistener() и синхронизация данных () . Когда наша платформа пользовательского интерфейса отобразит этот компонент, это гарантирует, что все три будут вызваны в правильном порядке.
8.1. Примеры в JVM
В AbstractList , AbstractSet, и AbstractMap , используемых коллекциями Java, есть много примеров этого шаблона. Например, методы indexOf() и lastIndexOf() работают в терминах метода ListIterator () , который имеет реализацию по умолчанию, но который переопределяется в некоторых подклассах. Равным образом, методы add(T) и addAll(int, T) работают в терминах метода add(int, T) , который не имеет реализации по умолчанию и должен быть реализован подклассом.
Java IO также использует этот шаблон в Входной поток , Выходной поток , Считыватель, и Писатель . Например, класс InputStream имеет несколько методов, которые работают в терминах read(byte [], int , int) , для реализации которых требуется подкласс.
9. Посетитель
Шаблон Посетитель позволяет нашему коду безопасно обрабатывать различные подклассы, не прибегая к проверкам instanceof . У нас будет интерфейс посетителя с одним методом для каждого конкретного подкласса, который нам необходимо поддерживать. Затем у нашего базового класса будет метод accept(Посетитель) . Каждый подкласс вызовет соответствующий метод для этого посетителя, передавая себя. Это затем позволяет нам реализовать конкретное поведение в каждом из этих методов, каждый из которых знает, что он будет работать с конкретным типом:
interface UserVisitor{ T visitStandardUser(StandardUser user); T visitAdminUser(AdminUser user); T visitSuperuser(Superuser user); }
class StandardUser { publicT accept(UserVisitor visitor) { return visitor.visitStandardUser(this); } }
Здесь у нас есть наш Пользовательский посетитель интерфейс с тремя различными методами посещения. В нашем примере Стандартный пользователь вызывает соответствующий метод, и то же самое будет сделано в Пользователь-администратор и Суперпользователь . Затем мы можем написать нашим посетителям, чтобы они работали с ними по мере необходимости:
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; } }
Наш Стандартный пользователь никогда не имеет разрешения, у вас Суперпользователя всегда есть разрешение, и у нашего Администратора может быть разрешение, но это нужно искать в самом пользователе.
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, поэтому мы можем видеть, как они используются таким образом, что многие приложения уже извлекают выгоду.