Connect with us

Разработка

Шаблон проектирования «Команда»

В этой статье раскроем суть паттерна с примерами на Swift и Kotlin.

Опубликовано

/

     
     

Шаблон проектирования «Команда» (Command) — это поведенческий паттерн, который превращает запрос или действие в объект. В мобильной разработке это один из самых полезных паттернов для реализации функций отмены действий (Undo/Redo), организации очередей задач и развязывания UI от бизнес-логики.

В этой статье раскроем суть паттерна с примерами на Swift и Kotlin.

Паттерн «Команда»

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

В контексте iOS и Android этот паттерн часто становится спасением, когда вы сталкиваетесь с перегруженными контроллерами (Massive View Controller/Activity), где обработка нажатий смешана с бизнес-логикой.

Зачем использовать?

  1. Undo/Redo: Самый частый кейс. Если каждое действие пользователя (например, «удалить письмо», «наложить фильтр») — это объект Command, то отмена действия — это просто вызов метода undo() у последнего объекта в стеке истории.
  2. Очереди задач (Task Queue): Команды можно сохранять в список и выполнять последовательно или в фоновом потоке. Это полезно для синхронизации данных или выполнения сетевых запросов.
  3. Макрокоманды: Можно объединить несколько команд в одну (Composite Command), чтобы одним нажатием выполнить цепочку действий (например, «Сохранить и Выйти»).

Структура паттерна

Основная идея: отделить объект, который инициирует действие, от объекта, который это действие выполняет.

Вместо прямого вызова метода мы создаём объект-команду, который:

  • знает, что нужно сделать;
  • знает, над кем выполнить действие;
  • может быть выполнен, сохранён, отменён или повторён.

Паттерн разделяет процесс на четыре основных участника:

  • Command (Интерфейс): Объявляет метод для выполнения (обычно execute()) и отмены (undo()).
  • Concrete Command (Конкретная команда): Реализует интерфейс. Связывает действие с получателем (Receiver).
  • Receiver (Получатель): Объект, который непосредственно выполняет бизнес-логику (например, DocumentEditor или MusicPlayer).
  • Invoker (Инициатор): Объект, который инициирует выполнение (например, кнопка на экране или жест свайпа). Он не знает, как выполняется команда, он просто вызывает execute().
  • Client (Клиент) — создаёт команды и связывает их с получателями

Реализация на Swift (iOS)

Рассмотрим пример простого текстового редактора, поддерживающего отмену ввода.

import Foundation

// 1. Receiver: Тот, кто реально выполняет работу
class TextEditor {
    private var text: String = ""

    func append(_ newText: String) {
        text += newText
        print("Editor content: \(text)")
    }

    func delete(length: Int) {
        text = String(text.dropLast(length))
        print("Editor content: \(text)")
    }
}

// 2. Command Protocol
protocol Command {
    func execute()
    func undo()
}

// 3. Concrete Command: Команда добавления текста
class AppendTextCommand: Command {
    private let editor: TextEditor
    private let textToAppend: String

    init(editor: TextEditor, text: String) {
        self.editor = editor
        self.textToAppend = text
    }

    func execute() {
        editor.append(textToAppend)
    }

    func undo() {
        editor.delete(length: textToAppend.count)
    }
}

// 4. Invoker: Менеджер команд (например, внутри ViewModel)
class CommandInvoker {
    private var history: [Command] = []

    func execute(_ command: Command) {
        command.execute()
        history.append(command)
    }

    func undo() {
        guard let lastCommand = history.popLast() else { return }
        lastCommand.undo()
    }
}

// Использование
let editor = TextEditor()
let invoker = CommandInvoker()

let command1 = AppendTextCommand(editor: editor, text: "Hello, ")
let command2 = AppendTextCommand(editor: editor, text: "World!")

invoker.execute(command1) // Editor content: Hello,
invoker.execute(command2) // Editor content: Hello, World!

invoker.undo()            // Editor content: Hello,

В данном примере CommandInvoker ничего не знает о том, как работать со строками, он просто управляет историей команд.

Реализация на Kotlin (Android)

В Android паттерн часто используется для обработки сложных пользовательских взаимодействий. Реализуем пример управления «Умным домом» (включение/выключение света).

// 1. Command Interface
interface Command {
    fun execute()
    fun undo()
}

// 2. Receiver: Устройство
class SmartLight(private val location: String) {
    fun turnOn() {
        println("$location light is ON")
    }

    fun turnOff() {
        println("$location light is OFF")
    }
}

// 3. Concrete Commands
class LightOnCommand(private val light: SmartLight) : Command {
    override fun execute() = light.turnOn()
    override fun undo() = light.turnOff()
}

class LightOffCommand(private val light: SmartLight) : Command {
    override fun execute() = light.turnOff()
    override fun undo() = light.turnOn()
}

// 4. Invoker: Пульт управления или экран приложения
class RemoteControl {
    private val commandHistory = ArrayDeque<Command>()

    fun submit(command: Command) {
        command.execute()
        commandHistory.addLast(command)
    }

    fun undo() {
        if (commandHistory.isNotEmpty()) {
            commandHistory.removeLast().undo()
        }
    }
}

// Использование
fun main() {
    val livingRoomLight = SmartLight("Living Room")
    val remote = RemoteControl()

    val turnOn = LightOnCommand(livingRoomLight)
    
    remote.submit(turnOn) // Output: Living Room light is ON
    remote.undo()         // Output: Living Room light is OFF
}

В Kotlin этот паттерн отлично сочетается с функциональными типами. Например, вместо создания классов команд, иногда можно передавать лямбды () -> Unit как упрощенную версию команд, если не требуется undo.

Преимущества и Недостатки

Плюсы

  • Принцип единственной ответственности (SRP): Отделяет классы, вызывающие операции, от классов, выполняющих эти операции.
  • Принцип открытости/закрытости (OCP): Можно добавлять новые команды, не ломая существующий код получателей или инициаторов.
  • Композиция: Позволяет собирать сложные команды из простых (макрокоманды).
  • Контроль: Легко реализовать логирование, отмену и повтор действий.

Минусы

  • Усложнение кода: Количество классов может значительно вырасти, так как для каждого действия создается отдельный класс. Для простых приложений это может быть избыточным (overengineering).

Command и архитектуры мобильных приложений

MVVM / MVI

  • Команды могут представлять user intents
  • Хорошо сочетаются с однонаправленным потоком данных

Clean Architecture

  • Command может быть обёрткой над Use Case
  • Упрощает передачу действий из слоя UI в домен

Redux-подобные подходы

  • Команды похожи на Actions, но с инкапсулированным поведением

Когда применять в мобильных приложениях?

Используйте паттерн «Команда», если:

  1. Вам нужна функция Undo/Redo (текстовые редакторы, графические редакторы)
  2. Вы создаете приложение с отложенными действиями (например, задачи выполняются при появлении сети)
  3. Вы хотите реализовать транзакционное поведение (если одна из серии команд упала, можно откатить все предыдущие)

Когда не стоит применять:

  • для простых CRUD-экранов
  • если команды не имеют состояния
  • когда достаточно обычных колбэков или лямбд

Итог

Шаблон проектирования Command — мощный инструмент для мобильной разработки, особенно в приложениях со сложным поведением и богатым пользовательским интерфейсом.

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

При разумном использовании Command отлично вписывается в современные архитектуры Android и iOS-приложений.

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: