1. Обзор
Шаблон команды является поведенческим шаблоном проектирования и является частью формального списка шаблонов проектирования GoF . Проще говоря, шаблон намеревается инкапсулировать в объект все данные, необходимые для выполнения данного действия (команды), включая то, какой метод вызывать, аргументы метода и объект, к которому принадлежит метод.
Эта модель позволяет нам отделять объекты , которые производят команды от их потребителей , поэтому шаблон обычно известен как шаблон производитель-потребитель.
В этом уроке мы узнаем, как реализовать шаблон команды в Java, используя как объектно-ориентированный, так и объектно-функциональный подходы, и посмотрим, в каких случаях он может быть полезен.
2. Объектно-ориентированная Реализация
В классической реализации шаблон команды требует реализации четырех компонентов: команды, Получателя, Вызывающего и Клиента .
Чтобы понять, как работает шаблон и какую роль играет каждый компонент, давайте создадим базовый пример.
Предположим, что мы хотим разработать приложение для текстовых файлов. В таком случае мы должны реализовать все функции, необходимые для выполнения некоторых операций, связанных с текстовыми файлами, таких как открытие, запись, сохранение текстового файла и так далее.
Итак, мы должны разбить приложение на четыре компонента, упомянутых выше.
2.1. Классы команд
Команда-это объект , роль которого заключается в хранении всей информации, необходимой для выполнения действия , включая вызываемый метод, аргументы метода и объект (известный как приемник), реализующий метод.
Чтобы получить более точное представление о том, как работают командные объекты, давайте начнем разработку простого командного уровня, который включает в себя только один интерфейс и две реализации:
@FunctionalInterface public interface TextFileOperation { String execute(); }
public class OpenTextFileOperation implements TextFileOperation { private TextFile textFile; // constructors @Override public String execute() { return textFile.open(); } }
public class SaveTextFileOperation implements TextFileOperation { // same field and constructor as above @Override public String execute() { return textFile.save(); } }
В этом случае интерфейс Операции с текстовым файлом определяет API командных объектов, а две реализации OpenTextFileOperation и SaveTextFileOperation выполняют конкретные действия. Первый открывает текстовый файл, а второй сохраняет текстовый файл.
Ясно видеть функциональность объекта команды: TextFileOperation команды инкапсулируют всю информацию, необходимую для открытия и сохранения текстового файла, включая объект получателя, методы для вызова и аргументы (в этом случае аргументы не требуются, но они могут быть).
Стоит подчеркнуть, что компонентом, выполняющим файловые операции, является получатель (экземпляр Текстового файла ) .
2.2. Класс Приемника
Приемник-это объект, который выполняет набор взаимосвязанных действий . Это компонент, который выполняет фактическое действие при вызове метода execute () |/команды.
В этом случае нам нужно определить класс приемника, роль которого заключается в моделировании Текстовый файл объекты:
public class TextFile { private String name; // constructor public String open() { return "Opening file " + name; } public String save() { return "Saving file " + name; } // additional text file methods (editing, writing, copying, pasting) }
2.3. Класс Вызывающего
Вызывающий-это объект, который знает, как выполнить данную команду, но не знает, как эта команда была реализована. Он знает только интерфейс команды.
В некоторых случаях вызывающий также хранит и ставит в очередь команды, помимо их выполнения. Это полезно для реализации некоторых дополнительных функций, таких как запись макросов или функция отмены и повтора.
В нашем примере становится очевидным, что должен существовать дополнительный компонент, ответственный за вызов объектов команд и их выполнение с помощью метода команд execute () . Именно здесь в игру вступает класс invoker .
Давайте рассмотрим базовую реализацию нашего призывателя:
public class TextFileOperationExecutor { private final ListtextFileOperations = new ArrayList<>(); public String executeOperation(TextFileOperation textFileOperation) { textFileOperations.add(textFileOperation); return textFileOperation.execute(); } }
Класс Text File OperationExecutor |/- это всего лишь тонкий слой абстракции, который отделяет объекты команд от их потребителей и вызывает метод, инкапсулированный в объекты TextFileOperation command.
В этом случае класс также хранит объекты команды в списке |. Конечно, это не обязательно при реализации шаблона, если только нам не нужно добавить какой-то дополнительный контроль в процесс выполнения операций.
2.4. Класс Клиента
Клиент-это объект, который управляет процессом выполнения команд , указывая, какие команды выполнять и на каких этапах процесса их выполнять.
Итак, если мы хотим быть ортодоксальными с формальным определением шаблона, мы должны создать клиентский класс, используя типичный метод main :
public static void main(String[] args) { TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation( new OpenTextFileOperation(new TextFile("file1.txt")))); textFileOperationExecutor.executeOperation( new SaveTextFileOperation(new TextFile("file2.txt")))); }
3. Объектно-Функциональная Реализация
До сих пор мы использовали объектно-ориентированный подход для реализации шаблона команд, и все это хорошо.
Начиная с Java 8, мы можем использовать объектно-функциональный подход, основанный на лямбда-выражениях и ссылках на методы, чтобы сделать код немного более компактным и менее подробным .
3.1. Использование лямбда-выражений
Поскольку интерфейс Операции с текстовым файлом является функциональным интерфейсом , мы можем передавать командные объекты в виде лямбда-выражений вызывающему , не создавая явно экземпляры TextFileOperation :
TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt"); textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt");
Реализация теперь выглядит гораздо более обтекаемой и лаконичной, так как мы сократили количество шаблонного кода .
Тем не менее, остается открытым вопрос: является ли этот подход лучше по сравнению с объектно-ориентированным?
Ну, это сложно. Если мы предположим, что более компактный код в большинстве случаев означает лучший код, то это действительно так.
Как правило, мы должны оценивать на основе каждого случая использования, когда следует прибегать к лямбда-выражениям .
3.2. Использование ссылок на методы
Аналогично, мы можем использовать ссылки на методы для передачи командных объектов вызывающему объекту:
TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); TextFile textFile = new TextFile("file1.txt"); textFileOperationExecutor.executeOperation(textFile::open); textFileOperationExecutor.executeOperation(textFile::save);
В этом случае реализация немного более подробна , чем та, которая использует лямбды , так как нам все еще нужно было создать экземпляры textFile .
4. Заключение
В этой статье мы изучили ключевые понятия шаблона команд и то, как реализовать шаблон в Java, используя объектно-ориентированный подход и комбинацию лямбда-выражений и ссылок на методы.
Как обычно, все примеры кода, показанные в этом руководстве, доступны на GitHub .