Команда – один из шаблонов проектирования поведения. Шаблон проектирования команды используется для реализации слабой связи в модели запрос-ответ.
Шаблон команды
В шаблоне команды запрос отправляется
invoker
, и invoker передает его инкапсулированному объекту command
. Объект команды передает запрос соответствующему методу Receiver
, чтобы выполнить конкретное действие. Клиентская программа создает объект получателя, а затем присоединяет его к команде. Затем она создает объект invoker и присоединяет объект команды для выполнения действия. Теперь, когда клиентская программа выполняет действие, оно обрабатывается на основе команды и объекта получателя.
Пример шаблона проектирования команды
Мы рассмотрим реальный сценарий, в котором можно применить шаблон “Команда”. Допустим, мы хотим создать утилиту для работы с файловой системой, которая будет иметь методы для открытия, записи и закрытия файлов. Эта утилита для файловой системы должна поддерживать несколько операционных систем, таких как Windows и Unix. Для реализации нашей утилиты для файловой системы нам сначала необходимо создать классы-получатели, которые фактически будут выполнять всю работу. Поскольку мы пишем код в терминах интерфейсов в языке Java, мы можем иметь интерфейс FileSystemReceiver и его реализацию для различных операционных систем, таких как Windows, Unix, Solaris и т.д.
Классы-получатели для шаблона “Команда”
package com.journaldev.design.command;
public interface FileSystemReceiver {
void openFile();
void writeFile();
void closeFile();
}
Интерфейс FileSystemReceiver определяет контракт для реализации классов. Для простоты я создаю два варианта классов-получателей для работы с системами Unix и Windows.
package com.journaldev.design.command;
public class UnixFileSystemReceiver implements FileSystemReceiver {
@Override
public void openFile() {
System.out.println("Opening file in unix OS");
}
@Override
public void writeFile() {
System.out.println("Writing file in unix OS");
}
@Override
public void closeFile() {
System.out.println("Closing file in unix OS");
}
}
package com.journaldev.design.command;
public class WindowsFileSystemReceiver implements FileSystemReceiver {
@Override
public void openFile() {
System.out.println("Opening file in Windows OS");
}
@Override
public void writeFile() {
System.out.println("Writing file in Windows OS");
}
@Override
public void closeFile() {
System.out.println("Closing file in Windows OS");
}
}
Вы заметили аннотацию @Override и если вам интересно, зачем она используется, пожалуйста, прочтите статьи аннотации в Java и преимущества аннотации @Override. Теперь, когда наши классы-получатели готовы, мы можем приступить к реализации наших классов-команд.
Интерфейс и реализации шаблона “Команда”
Мы можем использовать интерфейс или абстрактный класс для создания базовой команды, это зависит от требований проекта. Мы выбираем интерфейс, потому что у нас нет реализации по умолчанию.
package com.journaldev.design.command;
public interface Command {
void execute();
}
Теперь нам нужно создать реализации для всех различных типов действий, выполняемых получателем. Поскольку у нас есть три действия, мы создадим три реализации команд. Каждая реализация команды будет передавать запрос соответствующему методу получателя.
package com.journaldev.design.command;
public class OpenFileCommand implements Command {
private FileSystemReceiver fileSystem;
public OpenFileCommand(FileSystemReceiver fs){
this.fileSystem=fs;
}
@Override
public void execute() {
//команда "открыть" перенаправляет запрос к методу "открыть файл"
this.fileSystem.openFile();
}
}
package com.journaldev.design.command;
public class CloseFileCommand implements Command {
private FileSystemReceiver fileSystem;
public CloseFileCommand(FileSystemReceiver fs){
this.fileSystem=fs;
}
@Override
public void execute() {
this.fileSystem.closeFile();
}
}
package com.journaldev.design.command;
public class WriteFileCommand implements Command {
private FileSystemReceiver fileSystem;
public WriteFileCommand(FileSystemReceiver fs){
this.fileSystem=fs;
}
@Override
public void execute() {
this.fileSystem.writeFile();
}
}
Теперь у нас есть готовые получатель и реализации команд, поэтому мы можем перейти к реализации класса вызывающего объекта.
Класс вызывающего объекта шаблона “Команда”
Вызывающий объект – это простой класс, который инкапсулирует команду и передает запрос командному объекту для его обработки.
package com.journaldev.design.command;
public class FileInvoker {
public Command command;
public FileInvoker(Command c){
this.command=c;
}
public void execute(){
this.command.execute();
}
}
Наша реализация утилиты файловой системы готова, и мы можем приступить к написанию простой программы клиента с использованием шаблона команд. Но прежде я предоставлю утилитный метод для создания соответствующего объекта FileSystemReceiver
. Если мы можем использовать класс System для получения информации об операционной системе, то мы будем использовать его, в противном случае мы можем использовать шаблон Factory для возврата соответствующего типа на основе ввода.
package com.journaldev.design.command;
public class FileSystemReceiverUtil {
public static FileSystemReceiver getUnderlyingFileSystem(){
String osName = System.getProperty("os.name");
System.out.println("Underlying OS is:"+osName);
if(osName.contains("Windows")){
return new WindowsFileSystemReceiver();
}else{
return new UnixFileSystemReceiver();
}
}
}
Теперь давайте перейдем к созданию примера клиентской программы для шаблона команд, который будет использовать нашу утилиту файловой системы.
package com.journaldev.design.command;
public class FileSystemClient {
public static void main(String[] args) {
// Создание объекта получателя
FileSystemReceiver fs = FileSystemReceiverUtil.getUnderlyingFileSystem();
// Создание команды и связывание с получателем
OpenFileCommand openFileCommand = new OpenFileCommand(fs);
// Создание вызывающего объекта и связывание с командой
FileInvoker file = new FileInvoker(openFileCommand);
// Выполнение действия над объектом вызывающего
file.execute();
WriteFileCommand writeFileCommand = new WriteFileCommand(fs);
file = new FileInvoker(writeFileCommand);
file.execute();
CloseFileCommand closeFileCommand = new CloseFileCommand(fs);
file = new FileInvoker(closeFileCommand);
file.execute();
}
}
Обратите внимание, что клиент отвечает за создание соответствующего типа объекта команды. Например, если вы хотите записать файл, вам не следует создавать объект CloseFileCommand
. Клиентская программа также отвечает за присоединение получателя к команде, а затем команды к классу вызывающего. Результат работы приведенной выше программы примера шаблона команд следующий:
Underlying OS is:Mac OS X
Opening file in unix OS
Writing file in unix OS
Closing file in unix OS
Диаграмма классов шаблона команд
Вот диаграмма классов для нашей реализации утилиты файловой системы.
Важные моменты паттерна команд
- Команда является основой паттерна команд, который определяет контракт для реализации.
- Реализация получателя отдельна от реализации команды.
- Классы реализации команд выбирают метод для вызова на объекте получателя, для каждого метода получателя будет существовать реализация команды. Он работает как связь между получателем и методами действия.
- Класс Invoker просто перенаправляет запрос от клиента к объекту команды.
- Клиент ответственен за создание соответствующей команды и реализации получателя, а затем их связывание вместе.
- Клиент также ответственен за создание объекта Invoker и связывание объекта команды с ним, а затем выполнение метода действия.
- Паттерн команды легко расширяем, мы можем добавить новые методы действия в получателя и создать новые реализации команд без изменения кода клиента.
- Недостаток паттерна команд в том, что код становится громоздким и запутанным при большом количестве методов действий и из-за такого большого количества связей.
Пример использования паттерна команды в JDK
Интерфейс Runnable (java.lang.Runnable) и действие Swing (javax.swing.Action) используют паттерн команды.
Source:
https://www.digitalocean.com/community/tutorials/command-design-pattern