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

Шаблон команды в Java

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

1. Обзор

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

Эта модель позволяет нам отделить объекты, производящие команды, от их потребителей , поэтому шаблон широко известен как шаблон производитель-потребитель.

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

2. Объектно-ориентированная реализация

В классической реализации шаблон команды требует реализации четырех компонентов: Command, Receiver, Invoker и Client .

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

Предположим, что мы хотим разработать приложение для работы с текстовыми файлами. В таком случае мы должны реализовать все функциональные возможности, необходимые для выполнения некоторых операций, связанных с текстовыми файлами, таких как открытие, запись, сохранение текстового файла и т. д.

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

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();
}
}

В этом случае интерфейс TextFileOperation определяет API объектов команд, а две реализации, OpenTextFileOperation и SaveTextFileOperation, выполняют конкретные действия. Первый открывает текстовый файл, а второй сохраняет текстовый файл.

Функциональность командного объекта очевидна: команды TextFileOperation инкапсулируют всю информацию, необходимую для открытия и сохранения текстового файла, включая объект-получатель, вызываемые методы и аргументы (в данном случае аргументы не требуются, т.е. но они могут быть).

Стоит подчеркнуть, что компонент, выполняющий операции с файлами, является получателем ( экземпляром TextFile ) .

2.2. Класс получателя

Получатель — это объект, который выполняет набор связанных действий . Это компонент, который выполняет фактическое действие, когда вызывается метод команды execute() .

В этом случае нам нужно определить класс получателя, роль которого заключается в моделировании объектов TextFile :

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. Класс Invoker

Вызывающий объект — это объект, который знает, как выполнить данную команду, но не знает, как эта команда была реализована. Он знает только интерфейс команды.

В некоторых случаях инициатор также сохраняет и ставит в очередь команды, помимо их выполнения. Это полезно для реализации некоторых дополнительных функций, таких как запись макросов или функции отмены и повтора.

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

Давайте посмотрим на базовую реализацию нашего вызывающего объекта:

public class TextFileOperationExecutor {

private final List<TextFileOperation> textFileOperations
= new ArrayList<>();

public String executeOperation(TextFileOperation textFileOperation) {
textFileOperations.add(textFileOperation);
return textFileOperation.execute();
}
}

Класс TextFileOperationExecutor — это всего лишь тонкий слой абстракции, который отделяет объекты команд от их потребителей и вызывает метод, инкапсулированный в объектах команд TextFileOperation .

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

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 является функциональным интерфейсом , мы можем передавать объекты команд в виде лямбда-выражений вызывающей стороне без необходимости явно создавать экземпляры 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 .