Рубрики
Без рубрики

Поведенческие паттерны в ядре Java

Узнайте о некоторых наиболее распространенных шаблонах поведенческого проектирования с использованием Java.

Автор оригинала: Graham Cox.

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 {
    public  T 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, поэтому мы можем видеть, как они используются таким образом, что многие приложения уже извлекают выгоду.