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 .