В этом небольшом рецепте, я познакомлю вас с одним из поведенческих паттернов объектно-ориентированного программирования, который позволит вам инкапсулировать запросы в виде клиентских объектов, а также организовывать целые очереди таких «запросов на действие» с поддержкой их отмены и параметризации под конкретные нужды.
Для того, чтобы показать очень интересный пример, я опять прибегну к ситуативному описанию, взяв задачу и пример из уже упоминавшейся книги «Паттерны проектирования» Э.Фримена.
В нашем примере мы будем создавать целый API для пульта управления домашней автоматикой.
Пульт управления домашней автоматикой имеет четырнадцать кнопок, сгруппированных по две таким образом, что эти кнопки служат для включения и выключения чего-либо. Более того, эти кнопки не имеют каких-то начальных предустановок, т.е каждая группа кнопок может быть назначена на любое произвольное действие, которое может иметь только два состояния: включить или выключить некоторый компонент умного дома. Также, на пульте есть кнопка отмены последней команды, какой бы эта команда не была бы.
Очевидно, что каждая команда может быть представлена в виде самостоятельного класса, который и реализует осуществление самого запроса-команды (т.е внутрь класса помещается и сама команда и ее параметры). Также, хотелось бы сделать так, чтобы можно было помимо базовых команд, таких как включение/выключение света, телевизора и вентиляторов, можно было бы группе кнопок назначить целую последовательность команд (т.е создавать целые комплекты команды, так называемые макрокоманды или макросы).
Таким образом, мы видим, что команды фактически независимы друг от друга (каждая имеет свои параметры и разные цели для выполнения), а общим у них являются интерфейс и поведение. Помимо этого, сам пульт может и понятия не иметь о том, что делают команды, осуществляя просто их делегирование конкретным исполнителям…
Для того, чтобы обеспечить столь гибкое поведение, мы обратимся к паттерну «Команда» официальное определение которого выглядит так:
«Паттерн Команда инкапсулирует запрос в виде объекта, делая возможной параметризацию клиентских объектов с другими запросами, организацию очереди или регистрацию запросов, а также поддержку отмены операций»
(Паттерн имеет свою терминологию, с которой при желании вы можете ознакомится здесь)
В UML паттерн изображают так:
Полный код примера (со встроенными комментариями и юнит-тестированием):
module command; /* Пример паттерна проектирования "Команда" * * Краткое описание ситуации: * Требуется создать API для пульта управления домашней автоматикой. На пульте есть * четырнадцать кнопок, сгруппированных по две, при этом на одну такую группу назначаются * команды включения/выключения чего-либо. Кроме того, есть кнопка отмены последней * поданной команды. * * Официальное определение паттерна: * "Паттерн Команда инкапсулирует запрос в виде объекта, делая возможной параметризацию * клиентских объектов с другими запросами, организацию очереди или регистрацию запросов, * а также поддержку отмены операций." * * * Мои пояснения: * Некоторые вещи пришлось абсолютно переделать, кроме того, я добавил свои версии некоторых классов. */ // общий интерфейс всех команд public interface Command { public void execute(); public void undo(); } // команда-пустышка public class NoCommand : Command { void execute() { } void undo() { } } // команда включения света public class LightOnCommand : Command { private: Light light; public: this(Light light) { this.light = light; } void execute() { light.on(); } void undo() { light.off(); } } // команда выключения света public class LightOffCommand : Command { private: Light light; public: this(Light light) { this.light = light; } void execute() { light.off(); } void undo() { light.on(); } } import std.stdio; // свет class Light { private string location; this(string location) { this.location = location; } void on() { writefln("Light in %s is on", location); } void off() { writefln("Light in %s is off", location); } } // включить телевизор public class TVOnCommand : Command { private: TV tv; public: this(TV tv) { this.tv = tv; } void execute() { tv.on(); } void undo() { tv.off(); } } // выключить телевизор public class TVOffCommand : Command { private: TV tv; public: this(TV tv) { this.tv = tv; } void execute() { tv.off(); } void undo() { tv.on(); } } // телевизор class TV { private string location; this(string location) { this.location = location; } void on() { writefln("TV in %s is on", location); } void off() { writefln("TV in %s is off", location); } } // возможные скорости вентилятора enum FanSpeed : int { OFF, LOW, MEDIUM, HIGH, ULTRAHIGH }; // вентилятор public class CeilingFan { private: string location; int speed; public: this(string location) { this.location = location; speed = FanSpeed.OFF; } void off() { speed = FanSpeed.OFF; } void low() { speed = FanSpeed.LOW; } void medium() { speed = FanSpeed.MEDIUM; } void high() { speed = FanSpeed.HIGH; } void ultra() { speed = FanSpeed.ULTRAHIGH; } int getSpeed() { return speed; } string getLocation() { return location; } } // вентилятор почти на максимум class CeilingFanHighCommand : Command { private: CeilingFan ceilingFan; int previousSpeed; public: this(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } void execute() { previousSpeed = ceilingFan.getSpeed(); ceilingFan.high(); writefln("Ceiling fan in %s is high", ceilingFan.getLocation()); } // реализация отмены с состоянием void undo() { switch(previousSpeed) { case FanSpeed.OFF: ceilingFan.off(); writefln("Ceiling fan in %s is off", ceilingFan.getLocation()); break; case FanSpeed.LOW: ceilingFan.low(); writefln("Ceiling fan in %s is low", ceilingFan.getLocation()); break; case FanSpeed.MEDIUM: ceilingFan.medium(); writefln("Ceiling fan in %s is medium", ceilingFan.getLocation()); break; case FanSpeed.HIGH: ceilingFan.high(); writefln("Ceiling fan in %s is high", ceilingFan.getLocation()); break; case FanSpeed.ULTRAHIGH: ceilingFan.ultra(); writefln("Ceiling fan in %s is danger for you", ceilingFan.getLocation()); break; default: break; } } } // вентилятор на минимум class CeilingFanLowCommand : Command { private: CeilingFan ceilingFan; int previousSpeed; public: this(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } void execute() { previousSpeed = ceilingFan.getSpeed(); ceilingFan.low(); writefln("Ceiling fan in %s is low", ceilingFan.getLocation()); } void undo() { switch(previousSpeed) { case FanSpeed.OFF: ceilingFan.off(); writefln("Ceiling fan in %s is off", ceilingFan.getLocation()); break; case FanSpeed.LOW: ceilingFan.low(); writefln("Ceiling fan in %s is low", ceilingFan.getLocation()); break; case FanSpeed.MEDIUM: ceilingFan.medium(); writefln("Ceiling fan in %s is medium", ceilingFan.getLocation()); break; case FanSpeed.HIGH: ceilingFan.high(); writefln("Ceiling fan in %s is high", ceilingFan.getLocation()); break; case FanSpeed.ULTRAHIGH: ceilingFan.ultra(); writefln("Ceiling fan in %s is danger for you", ceilingFan.getLocation()); break; default: break; } } } // макрокоманды - команды состоящие из множества команд public class MacroCommand : Command { private Command[] commands; public: this(Command[] commands) { this.commands = commands; } void execute() { for (size_t i = 0; i < commands.length; i++) { commands[i].execute(); } } void undo() { for (size_t i = 0; i < commands.length; i++) { commands[i].undo(); } } } // пульт дистанционного управления домашней автоматикой public class RemoteControlWithUndo { private: Command[] onCommands, offCommands; Command undoCommand; public: this() { onCommands = new Command[7]; offCommands = new Command[7]; Command noCommand = new NoCommand(); for (size_t i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } // назначить команды слоту void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } // нажата некоторую кнопку включения void onButtonPressed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } // нажать некоторую кнопку выключения void offButtonPressed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } // нажать кнопку отмены void undoButtonPressed() { undoCommand.undo(); } } unittest { writeln("--- Command test ---"); RemoteControlWithUndo remoteControl = new RemoteControlWithUndo(); Light light1 = new Light("Living Room"); Light light2 = new Light("Bath Room"); Light light3 = new Light("Watercloset"); CeilingFan ceilingFan = new CeilingFan("Living Room"); TV tv1 = new TV("Living Room"); TV tv2 = new TV("Bath Room"); LightOnCommand lightOn1 = new LightOnCommand(light1); LightOnCommand lightOn2 = new LightOnCommand(light2); LightOnCommand lightOn3 = new LightOnCommand(light3); LightOffCommand lightOff1 = new LightOffCommand(light1); LightOffCommand lightOff2 = new LightOffCommand(light2); LightOffCommand lightOff3 = new LightOffCommand(light3); CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan); CeilingFanLowCommand ceilingFanLow = new CeilingFanLowCommand(ceilingFan); TVOnCommand tvOn1 = new TVOnCommand(tv1); TVOffCommand tvOff1 = new TVOffCommand(tv1); TVOnCommand tvOn2 = new TVOnCommand(tv2); TVOffCommand tvOff2 = new TVOffCommand(tv2); Command[] commands1 = cast(Command[]) [lightOn1, lightOn2, lightOn3, tvOn1, tvOn2]; Command[] commands2 = cast(Command[]) [lightOff1, lightOff2, lightOff3, tvOff1, tvOff2]; MacroCommand allOn = new MacroCommand(commands1); MacroCommand allOff = new MacroCommand(commands2); remoteControl.setCommand(0, lightOn1, lightOff1); remoteControl.setCommand(1, lightOn2, lightOff2); remoteControl.setCommand(2, lightOn3, lightOff3); remoteControl.setCommand(3, ceilingFanHigh, ceilingFanLow); remoteControl.setCommand(4, tvOn1, tvOff1); remoteControl.setCommand(5, tvOn2, tvOff2); remoteControl.setCommand(6, allOn, allOff); writeln("Fan:"); remoteControl.onButtonPressed(3); // включить вентилятор writeln("Light:"); remoteControl.onButtonPressed(1); // включить в комнате writeln("Undo:"); remoteControl.undoButtonPressed(); // отменить writeln("All On:"); remoteControl.onButtonPressed(6); // включить все writeln("All Off:"); remoteControl.offButtonPressed(6); // выключить все writeln("Undo:"); remoteControl.undoButtonPressed(); // отменить writeln("TV:"); remoteControl.offButtonPressed(4); // выключить телевизор в комнате }
Стоит отметить, что паттерн используется довольно часто: для организации записи макросов, многоуровневой отмены операций, создания потоков и пулов выполнения, реализации индикаторов последовательного выполнения действий, создания цепочек транзакций и многих других полезных вещей.
Более подробно с паттерном вы можете познакомится в уже упомянутой выше книге, а также запустив в режиме юнит-тестирования приведенный код примера (почитав комментарии к коду) и посмотрев его реализацию непосредственно в нашем репозитории реализаций паттернов, который можно полностью скопировать на свой компьютер.