Swift Design Patterns: Command

Зачем нужен?

Паттерн Command применяется, когда необходимо выполнять команды(операции, запросы), без знания того, какие функции должны вызываться и даже того, кто должен выполнять эти функции. Паттерн инкапсулирует вычислительные блоки(получателя + набор операций). Его можно применять в планировщиках, различных пулах, очередях задач.

Обеспечивает соблюдение принципа инверсии зависимостей. Уменьшает связанность классов.

Определение

  • инкапсулирует запрос в виде объекта, делая возможной параметризацию клиентских объектов с различными запросами, организацию очереди или регистрацию запросов, а также поддержку отмены операций

Реализация

Command разъединяет(decouple) объект, который вызывает операцию от объекта, который знает как эту операцию выполнить. Для этого создается интерфейс Command, который связывает получателя с действием. Интерфейс содержит абстрактный метод execute, который вызывает действия у получателя. Клиенты Command рассматривают получателя как черную коробку: когда необходимо, клиент вызывает метод execute и больше его ничего не волнует.

Клиент, создающий команду и клиент, выполняющий команду - разные клиенты. Данное разделение обеспечивает гибкость во времени выполнения и последовательности команд. Инкапсуляция команд в виде объекто делает возможным их передачу/хранение/загрузку и тд.

  1. Определите интерфейс команды с сигнатурой метода такой как execute()
  2. Создайте несколько конкретных классов комманд, содержащих объект получателя, методы, которые нужно вызвать и аргументы, которые нужно передать получателю.
  3. Инициализируйте объекты команд для каждого отложенного выполнения
  4. Передайте объекты команд от создателя(отправителя, sender) к инициатору(invoker)
  5. Инициатор определяет, когда выполнять команды

Пример

Допустим у нас есть пульт управления умным домом. В нем есть возможность задать несколько устройств, и для каждого устройства есть кнопка on и off.

Определим интерфейс команды

// абстрактная команда, используем для инкапсуляции классов устройств(устройство + операции)
// в результате при добавлении новых классов устройств
// не нужно будет изменять управляющий класс пульта(remoteControl)
protocol Command {
    func execute()
}

Создадим несколько конкретных классов команд

// классы конкретных комманд, инкапсулируют запросы к конкретным устройствам
class NoCommand: Command {
    // объект-получатель
    func execute() {
        print("No command")
    }
}

class LightOnCommand: Command {
    // объект-получатель
    private let light: Light
    
    init(light: Light) {
        self.light = light
    }
    
    func execute() {
        // методы, которые нужно вызвать, без аргументов
        light.on()
    }
}

class LightOffCommand: Command {
    // объект-получатель
    private let light: Light
    
    init(light: Light) {
        self.light = light
    }
    
    func execute() {
        // методы, которые нужно вызвать, без аргументов
        light.off()
    }
}

class StereoOnWithCDCommand: Command {
    // объект-получатель
    private let stereo: Stereo
    
    init(stereo: Stereo) {
        self.stereo = stereo
    }
    
    func execute() {
        // методы, которые нужно вызвать, без аргументов
        stereo.on()
        stereo.setCD()
        stereo.setVolume()
    }
}

class GarageOpenCommand: Command {
    // объект-получатель
    private let garage: Garage
    
    init(garageDoor: Garage) {
        self.garage = garageDoor
    }
    
    func execute() {
        // методы, которые нужно вызвать, без аргументов
        garage.openDoor()
    }
}

class GarageCloseCommand: Command {
    // объект-получатель
    private let garage: Garage
    
    init(garageDoor: Garage) {
        self.garage = garageDoor
    }
    
    func execute() {
        // методы, которые нужно вызвать, без аргументов
        garage.closeDoor()
    }
}

Инициализируем объекты команд

let livingRoomLight = Light(site: "Living Room")
let livingRoomLightOn = LightOnCommand(light: livingRoomLight)
let livingRoomLightOff = LightOffCommand(light: livingRoomLight)

let kitchenLight = Light(site: "Kitchen")
let kitchenLightOn = LightOnCommand(light: kitchenLight)
let kitchenLightOff = LightOffCommand(light: kitchenLight)

let garage = Garage()
let garageOpen = GarageOpenCommand(garageDoor: garage)
let garageClose = GarageCloseCommand(garageDoor: garage)

Передадим объекты команд от создателя(creator) к инициатору(invoker)

let remote = RemoteControl()
remote.setCommand(0, onCommand: livingRoomLightOn, offCommand: livingRoomLightOff)
remote.setCommand(1, onCommand: kitchenLightOn, offCommand: kitchenLightOff)
remote.setCommand(2, onCommand: garageOpen, offCommand: garageClose)

В нашем случае команды выполняются по нажатию кнопок на пульте управления

remote.onButtonPressed(0)
remote.offButtonPressed(0)
remote.undoButtonPressed()

remote.onButtonPressed(1)
remote.offButtonPressed(1)

remote.onButtonPressed(2)
remote.offButtonPressed(2)

remote.onButtonPressed(3)
remote.offButtonPressed(3)

В качестве другого примера можно рассмотреть ресторан. Командой в данном случае будет заказ. Посетитель - клиент-создатель(client, creator), официантка - инициатор, повар - получатель.

Реализация паттерна Command: