Insertion Sort, Merge Sort

Insertion Sort

Сортировка вставками – каждый элемент сравнивается с предыдущими по списку и элемент меняется местами с большим, если таковой имеется, в ином случае внутренний цикл сравнения останавливается. Так как элементы сортируются с первого по последний, то каждый элемент сравнивается с уже отсортированным списком, что *возможно* уменьшит общее время работы. Временная сложность алгоритма O(n^2), то есть идентична баббл сорту.

Merge Sort

Сортировка слиянием – список разделяется на группы по одному элементу, затем группы “сливаются” попарно с одновременным сравнением. В моей реализации при слиянии пар элементы слева сравниваются с элементами справа, затем перемещаются в результирующий список, если элементы слева закончились, то происходит добавление всех элементов справа в результирующий список (их дополнительное сравнение излишне, так как все элементы в группах проходят итерации сортировки)
Работу данного алгоритма очень легко распараллелить, этап слияния пар можно выполнять в потоках, с ожиданием окончания итераций в диспетчере.
Вывод алгоритма для однопоточного выполнения:


["John", "Alice", "Mike", "#1", "Артем", "20", "60", "60", "DoubleTrouble"]
[["John"], ["Alice"], ["Mike"], ["#1"], ["Артем"], ["20"], ["60"], ["60"], ["DoubleTrouble"]]
[["Alice", "John"], ["#1", "Mike"], ["20", "Артем"], ["60", "60"], ["DoubleTrouble"]]
[["#1", "Alice", "John", "Mike"], ["20", "60", "60", "Артем"], ["DoubleTrouble"]]
[["#1", "20", "60", "60", "Alice", "John", "Mike", "Артем"], ["DoubleTrouble"]]
["#1", "20", "60", "60", "Alice", "DoubleTrouble", "John", "Mike", "Артем"]

Вывод алгоритма для многопоточного выполнения:


["John", "Alice", "Mike", "#1", "Артем", "20", "60", "60", "DoubleTrouble"]
[["John"], ["Alice"], ["Mike"], ["#1"], ["Артем"], ["20"], ["60"], ["60"], ["DoubleTrouble"]]
[["20", "Артем"], ["Alice", "John"], ["60", "60"], ["#1", "Mike"], ["DoubleTrouble"]]
[["#1", "60", "60", "Mike"], ["20", "Alice", "John", "Артем"], ["DoubleTrouble"]]
[["DoubleTrouble"], ["#1", "20", "60", "60", "Alice", "John", "Mike", "Артем"]]
["#1", "20", "60", "60", "Alice", "DoubleTrouble", "John", "Mike", "Артем"]

Временная сложность алгоритма O(n*log(n)), что немного лучше чем O(n^2)

Источники

https://en.wikipedia.org/wiki/Insertion_sort
https://en.wikipedia.org/wiki/Merge_sort

Исходный код

https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/insertionSort
https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/mergeSort

Сортировка пузырьком на Erlang

Сортировка пузырьком это достаточно скучно, но становится интереснее если попробовать реализовать его на функциональном языке для телекома – Erlang.

У нас есть список из цифр, нам нужно его отсортировать. Алгоритм сортировки пузырьком проходит по всему списку, итерируя и сравнивая числа попарно. На проверке происходит следующее: меньшее число добавляется в выходной список, либо числа меняются местами в текущем списке если справа меньше, перебор продолжается со следующим по итерации числом. Данный обход повторяется до тех пор, пока в списке больше не будет замен.

На практике его использовать не стоит из-за большой временной сложности алгоритма – O(n^2); я реализовал его на языке Erlang, в императивном стиле, но если вам интересно то можете поискать лучшие варианты:


-module(bubbleSort).
-export([main/1]).

startBubbleSort([CurrentHead|Tail]) ->
    compareHeads(CurrentHead, Tail, [], [CurrentHead|Tail]).

compareHeads(CurrentHead, [NextHead|Tail], [], OriginalList) ->   
    if
        CurrentHead < NextHead ->
            compareHeads(NextHead, Tail, [CurrentHead], OriginalList);
        true ->
            compareHeads(CurrentHead, Tail, [NextHead], OriginalList)
    end;
    
compareHeads(CurrentHead, [NextHead|Tail], OriginalOutputList, OriginalList) ->
    if
        CurrentHead < NextHead ->
            OutputList = OriginalOutputList ++ [CurrentHead],
            compareHeads(NextHead, Tail, OutputList, OriginalList);
        true ->
            OutputList = OriginalOutputList ++ [NextHead],
            compareHeads(CurrentHead, Tail, OutputList, OriginalList)
    end;
  
compareHeads(CurrentHead, [], OriginalOutputList, OriginalList) ->
    OutputList = OriginalOutputList ++ [CurrentHead],
    if
        OriginalList == OutputList ->
            io:format("OutputList: ~w~n", [OutputList]);
        true ->
            startBubbleSort(OutputList)
    end.
  
main(_) ->
    UnsortedList = [69,7,4,44,2,9,10,6,26,1],
    startBubbleSort(UnsortedList).

Установка и запуск

В Ubuntu Эрланг установить очень просто, достаточно в терминале набрать команду sudo apt install erlang. В данном языке каждый файл должен представлять из себя модуль (module), со списком функций которые можно использовать извне – export. К интересным особенностям языка относится отсутствие переменных, только константы, отсутствие стандартного синтаксиса для ООП (что не мешает использовать ООП техники), и конечно же параллельные вычисления без блокировок на основе модели акторов.

Запустить модуль можно либо через интерактивную консоль erl, запуская одну команду за другой, либо проще через escript bubbleSort.erl; Для разных случаев файл будет выглядеть по разному, например для escript необходимо сделать функцию main, из которой он будет стартовать.

Источники

https://www.erlang.org/
https://habr.com/ru/post/197364/

Исходный код

https://gitlab.com/demensdeum/algorithms/blob/master/bubbleSort/bubbleSort.erl

Алгоритм лексикографического сравнения

Алгоритм лексикографического сравнения строк работает очень просто, в цикле происходит сравнение кодов символов и возвращается результат если символы не равны.

Пример для языка Си можно посмотреть здесь:
https://github.com/gcc-mirror/gcc/blob/master/libiberty/memcmp.c

Следует учитывать что сравнивать символы нужно в единой статичной кодировке, например в Swift я использовал посимвольное сравнение на UTF-32. Вариант сортировки массива с использованием memcmp сработает точно для однобайтовых символов, в остальных случаях (кодировки переменной длины) возможно порядок будет некорректен. Не исключаю возможности реализации на основе кодировок переменной длины, но скорее всего будет на порядок сложнее.

Временная сложность алгоритма в лучшем случае O(1), среднем и худшем O(n)

Источники

https://ru.wikipedia.org/wiki/Лексикографический_порядок

Исходники

https://gitlab.com/demensdeum/algorithms/blob/master/lexiCompare/lexiCompare.swift

Разработка игры для ZX Spectrum на C

Эта непутевая заметка посвящена разработке игры для старого компьютера ZX Spectrum на C. Давайте взглянем на красавца:

Он начал производится в 1982 году, и выпускался до 1992 года. Технические характеристики машины: 8-битный процессор Z80, 16-128кб памяти и прочие экстеншены, например звуковой чип AY-3-8910.

В рамках конкурса Yandex Retro Games Battle 2019 для данной машины я написал игру под названием Interceptor 2020. Так как учить ассемблер для Z80 времени не было, я решил разработать ее на языке Си. В качестве тулчейна я выбрал готовый набор – z88dk, который содержит компиляторы Си, и много вспомогательных библиотек для ускорения реализации приложений для Спектрума. Он также поддерживает множество других Z80 машин, например MSX, калькуляторов Texas Instruments.

Далее я опишу свой поверхностный полет над архитектурой компьютера, тулчейном z88dk, покажу как удалось реализовать ООП подход, использовать паттерны проектирования.

Особенности установки

Установку z88dk следует проводить по мануалу из репозитория, однако для пользователей Ubuntu я хотел бы отметить особенность – если у вас уже установлены компиляторы для Z80 из deb пакетов, то следует их удалить, так как z88dk по умолчанию будет обращаться к ним из папки bin, из-за несовместимости версий тулчейн-компилятор вы скорее всего ничего не сможете собрать.

Hello World

Написать Hello World очень просто:


#include 

void main()
{
    printf("Hello World");
}

Собрать в tap файл еще проще:


zcc +zx -lndos -create-app -o helloworld helloworld.c

Для запуска используйте любой эмулятор ZX Spectrum с поддержкой tap файлов, например онлайн:
http://jsspeccy.zxdemo.org/

Рисуем на картинку на весь экран

tl;dr Картинки рисуются тайлами, тайлами размера 8×8 пикселей, сами тайлы встраиваются в шрифт спектрума, затем строкой из индексов печатается картинка.

Библиотека вывода спрайтов и тайлов sp1 выводит тайлы с помощью UDG. Картинка переводится в набор отдельных UDG (тайлы), затем собирается на экране с помощью индексов. Следует помнить что UDG используется для вывода текста, и если ваша картинка содержит очень большой набор тайлов (например больше 128 тайлов), то придется выходить за границы набора и стирать дефолтный спектрумовский шрифт. Чтобы обойти это ограничение, я использовал базу от 128 – 255 с помощью упрощения изображений, оставляя оригинальный шрифт на месте. Об упрощении картинок ниже.

Для отрисовки полноэкранных картинок нужно вооружиться тремя утилитами:
Gimp
img2spec
png2c-z88dk

Есть путь настоящих ZX мужчин, настоящих ретро-воинов это открыть графический редактор, используя палитру спектрума, зная особенности вывода картинки, подготовить ее вручную и выгрузить с помощью png2c-z88dk или png2scr.

Путь попроще – взять 32 битное изображение, переключить в Gimp количество цветов до 3-4-х, слегка подредактировать, затем импортировать в img2spec для того чтобы не работать с цветовыми ограничениями вручную, экспортировать png и перевести в Си массив с помощью png2c-z88dk.

Следует помнить что для успешного экспорта каждый тайл не может содержать больше двух цветов.

В результате вы получите h файл, который содержит количество уникальных тайлов, если их больше ~128, то упрощайте в Gimp картинку (увеличьте повторяемость) и проводите процедуру экспорта по новой.

После экспорта вы в прямом смысле загружаете “шрифт” из тайлов и печатаете “текст” из индексов тайлов на экране. Далее пример из “класса” рендера:


// грузим шрифт в память
    unsigned char *pt = fullscreenImage->tiles;

    for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
            sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
    }

    // ставим курсор в 0,0
    sp1_SetPrintPos(&ps0, 0, 0);

    // печатаем строку
    sp1_PrintString(&ps0, fullscreenImage->ptiles);

Рисуем спрайты на экране

Далее я опишу способ рисования спрайтов 16×16 пикселей на экране. До анимации и смены цветов я не дошел, т.к. банально уже на этом этапе, как я предполагаю, у меня кончилась память. Поэтому в игре присутствуют только прозрачные монохромные спрайты.

Рисуем в Gimp монохромную png картинку 16×16, далее с помощью png2sp1sprite переводим ее в ассемблерный файл asm, в Си коде объявляем массивы из ассемблерного файла, добавляем файл на этапе сборки.

После этапа объявления ресурса спрайта, его надо добавить на экран в нужную позицию, далее пример кода “класса” игрового объекта:


    struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2,    SP1_TYPE_2BYTE, col2-col1, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB,  SP1_TYPE_2BYTE, 0, 0);
    sp1_IterateSprChar(bubble_sprite, initialiseColour);

По названиям функций можно примерно понять смысл – аллоцируем память для спрайта, добавляем две колонки 8×8, добавляем цвет для спрайта.

В каждом кадре проставляется позиция спрайта:


sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);

Эмулируем ООП

В Си нет синтаксиса для ООП, что же делать если все равно очень хочется? Надо подключить думку и озариться мыслью что такой вещи как ООП оборудование не существует, все в итоге приходит к одной из машинных архитектур, в которых просто нет понятия объекта и прочих связанных с этим абстракций.

Данный факт мне очень долго мешал понять зачем вообще нужно ООП, почему нужно его использовать если в итоге все приходит к машинному коду.

Однако поработав в продуктовой разработке, мне открылись прелести этой парадигмы программирования, в основном конечно гибкость разработки, защитные механизмы кода, при правильном подходе уменьшение энтропии, упрощение работы в команде. Все перечисленные преимущества вытекают из трех столпов – полиморфизма, инкапсуляции, наследования.

Также стоит отметить упрощение решения вопросов связанных с архитектурой приложения, ведь 80% архитектурных проблем было решено компьютерными-учеными еще в прошлом веке и описано в литературе посвященной паттернам проектирования. Далее я опишу способы добавить похожий на ООП синтаксис в Си.

За основу хранения данных экземпляра класса удобнее взять структуры Си. Конечно можно использовать байтовый буфер, создать свою собственную структуру для классов, методов, но зачем переизобретать колесо? Ведь мы и так переизобретаем синтаксис.

Данные класса

Пример полей данных “класса” GameObject:


struct GameObjectStruct {
    struct sp1_ss *gameObjectSprite;
    unsigned char *sprite_col;
    unsigned char x;
    unsigned char y;
    unsigned char referenceCount;
    unsigned char beforeHideX;
    unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;

Сохраняем наш класс как “GameObject.h” делаем #include “GameObject.h” в нужном месте и пользуемся.

Методы класса

Возьмем на вооружение опыт разработчиков языка Objective-C, сигнатура метода класса будут представлять из себя функции в глобальном скопе, первым аргументом всегда будет передаваться структура данных, далее идут аргументы метода. Далее пример “метода” “класса” GameObject:


void GameObject_hide(GameObject *gameObject) {
    gameObject->beforeHideX = gameObject->x;
    gameObject->beforeHideY = gameObject->y;
    gameObject->y = 200;
}

Вызов метода выглядит так:


GameObject_hide(gameObject);

Конструкторы и деструкторы реализуются таким же образом. Можно реализовать конструктор как аллокатор и инициализатор полей, однако мне больше нравится осуществлять контроль над аллокацией и инициализацей объектов раздельно.

Работа с памятью

Ручное управление памятью вида с помощью malloc и free обернутых в макросы new и delete для соответствия с C++:


#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)

Для объектов которые используются несколькими классами сразу, реализовано полу-ручное управление памятью на основе подсчета ссылок, по образу и подобию старого механизма Objective-C Runtime ARC:


void GameObject_retain(GameObject *gameObject) {
    gameObject->referenceCount++;
}

void GameObject_release(GameObject *gameObject) {
    gameObject->referenceCount--;

    if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
        sp1_DeleteSpr(gameObject->gameObjectSprite);
        delete(gameObject);
    }
}

Таким образом каждый класс должен объявлять использование общего объекта с помощью retain, освобождать владение через release. В современном варианте ARC используется автоматическое проставление вызовов retain/release.

Звучим!

На Спектруме есть пищалка способная воспроизводить 1-битовую музыку, композиторы того времени умели воспроизводить на ней до 4-х звуковых каналов одновременно.

Spectrum 128k содержит отдельный звуковой чип AY-3-8910, на котором можно воспроизводить трекерную музыку.

Для использования пищалки в z88dk предлагается библиотека

Что предстоит узнать

Мне было интересно ознакомиться со Спектрумом, реализовать игру средствами z88dk, узнать много интересных вещей. Многое мне еще предстоит изучить, например ассемблер Z80, так как он позволяет использовать всю мощь Спектрума, работу с банками памяти, работу со звуковым чипом AY-3-8910. Надеюсь поучаствовать в конкурсе на следующий год!

Ссылки

https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/

Исходный код

https://gitlab.com/demensdeum/zx-projects/tree/master/interceptor2020

Двоичный поиск

Допустим нам необходимо узнать относится ли адрес электронной почты “demensdeum@gmail.com”  к списку разрешенных email адресов для получения писем.

Переберем весь список от первого до последнего элемента, проверяя равен ли элемент указанному адресу – реализуем алгоритм линейного поиска. Но это же будет долго, или не будет?

Для ответа на этот вопрос используют “Временную сложность алгоритмов”, “О” нотацию. Время работы линейного поиска в худшем случае равно n-му количеству элементов массива, напишем это в “О” нотации – O(n). Далее нужно пояснить что для любого известного алгоритма есть три показателя производительности – время выполнения в лучшем, худшем и среднем случае. Например адрес почты “demensdeum@gmail.com” находится в первом индексе массива, тогда он будет найден за первый шаг алгоритма, из этого следует что время выполнения в лучшем случае – O(1); а если в конце списка, то это худший случай – O(n)

Но как же детали реализации ПО, производительность железа, они ведь должны влиять на big O? А теперь выдохните и представьте что расчет временной сложности рассчитывается для некой абстрактной идеальной машины, в которой есть только этот алгоритм и больше ничего.

Алгоритм

Ок, получается что линейный поиск достаточно медленный, попробуем использовать Бинарный поиск. Для начала следует пояснить что с бинарными данными мы работать не будем, такое название данному методу дано из-за особенностей его работы. Изначально мы сортируем массив в лексикографическом порядке, затем алгоритм берет диапазон всего массива, получает средний элемент диапазона, сравнивает его лексикографически, и в зависимости от результата сравнения решает какой диапазон брать для поиска дальше – верхнюю половину текущего или нижнюю. То есть на каждом шаге поиска принимается решение из двух возможных – бинарная логика. Этот шаг повторяется до тех пор, пока либо слово найдется, либо не найдется (произойдет пересечение нижнего и верхних индексов диапазона).

Производительность данного алгоритма – лучший случай когда сразу найден элемент в середине массива O(1), худший случай перебора O(log n)

Подводные камни

При реализации бинарного поиска я встретился мало того с интересной проблемой отсутствия стандартизации лексикографического сравнения в библиотеках языков программирования, но даже обнаружил отсутствие единого стандарта реализации localeCompare внутри JavaScript. Стандарт ECMAScript допускает разную реализацию данной функции, из-за этого при сортировке с помощью localeCompare, на разных движках JavaScript может наблюдаться абсолютно разный результат.

Поэтому для корректной работы алгоритма нужно обязательно сортировать и использовать в работе только один и тот же алгоритм лексикографического сравнения, иначе работать ничего не будет. Со-но если например попытаться сортировать массив в Scala, а искать с помощью nodejs, не реализуя собственную сортировку/сортировку одной реализации, то кроме разочарования в человечестве вас ничего не ждет.

Источники

Что такое лексикографическое сравнение и что оно собой представляет?
Почему для вычисления сложности алгоритмов используется log N вместо lb N?
Двоичный поиск
Знай сложности алгоритмов
https://stackoverflow.com/questions/52941016/sorting-in-localecompare-in-javascript

Исходный код

https://gitlab.com/demensdeum/algorithms

Паттерн Фасад


Фасад относится к структурным паттернам проектирования. Он предоставляет единый интерфейс, обеспечивающий работу со сложными системами, позволяя клиентам не обладать деталями реализации о данных системах, упрощать таким образом свой код, реализовывать слабую связанность между клиентами и системами нижнего уровня. В GoF есть хороший пример Фасада – компилятор языков программирования, предоставляющий разным клиентам, преследующим разные цели, возможность сборки кода через единый интерфейс фасада-компилятора.

Источники

https://refactoring.guru/ru/design-patterns/facade
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Паттерн Абстрактная Фабрика

Абстрактная фабрика – предоставляет интерфейс создания связанных объектов, без указания конкретных классов.

Мне большие нравится альтернативное название данного паттерна – Набор (Kit)

Он очень похож на Фабричный Метод, однако Абстрактные Фабрики должны описывать связь между создаваемыми объектами, иначе это уже просто антипаттерн God Object, создающий бессистемно все подряд.

Представим себе разработку AR фреймворка для очков, мы выводим на экране стрелки indoor навигации, иконки магазинов, интересных мест, окна и кнопки с информацией о каком-либо месте, в котором сейчас находится пользователь.

При этом нам нужна возможность кастомизировать внешний вид и поведение контролов AR окружения. Вот именно для этого случая нужно использовать паттерн Набор.

Напишем интерфейс Абстрактной Фабрики и Абстрактных Продуктов – родительских протоколов, элементов AR окружения:


protocol ARFactory {
    func arrow() -> ARArrow
    func icon() -> ARIcon
    func button() -> ARButton
    func window() -> ARWindow
}

protocol ARArrow {
    var image: { get }
    func handleSelection()
}

protocol ARIcon {
    var image: { get }
    var title: String
}

protocol ARButton {
    var title: String
    func handleSelection()
}

protocol ARWindow {
    var title: String
    var draw(canvas: Canvas)
}

Теперь разработчикам наборов нужно будет реализовать Конкретную Фабрику на основе интерфейса Абстрактной Фабрики, причем реализовать придется все элементы вместе, остальные части приложения смогут работать с фабрикой не меняя свой код.

Источники

https://refactoring.guru/ru/design-patterns/abstract-factory
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Фабричный Метод

Паттерн Фабричный Метод относится к порождающим паттернам проектирования.
Данный паттерн описывает создание интерфейса для создания объекта конкретного класса. Вроде просто да?

В теории

Допустим мы разрабатываем фреймворк для работы с AR очками, при наклоне головы на бок, перед глазами пользователя должно появляться меню доступных приложений. Приложения будут разрабатывать сторонние компании, клиенты нашего фреймворка. Естественно мы не знаем какие именно приложения, значки, названия должны появляться, поэтому мы должны предоставить интерфейс для реализации значка и сопутствующей информации о приложении. Назовем его Продуктом:


protocol Product {
 var name: String { get }
 var image: Image { get }
 var executablePath: String { get }
}

Далее нам нужно предоставить интерфейс для того чтобы наши клиенты реализовали выдачу массива приложений своего Конкретного Продукта – массив значков приложений с названиями, который мы уже отрисуем во фреймворке.

Напишем этот интерфейс – интерфейс Создателя, содержащий Фабричный Метод, отдающий массив Продуктов.


protocol Creator {
 func factoryMethod() -> [Product]
}

На практике

Первым клиентом нашего AR фреймворка стала компания 7Б – ведущий поставщик софта для кофеварок в Гондурасе. Они хотят продавать очки дополненной реальности с возможностью заваривать кофе, проверять заполненность воды/зерен, указывать дорогу к их ближайшей кофеварке в режиме indoor карт.

Разработку софта они берут на себя, от нас требуется только предоставить документацию по интерфейсам Создателя и Продукта, для корректного вывода списка приложений и их дальнейшего запуска.

После передачи документации, компания 7Б используя интерфейс Создателя реализует Конкретного Создателя – класс возвращающий массив приложений-значков. Сами приложения-значки представляют из себя классы Конкретного Продукта имплементирующие интерфейс Продукта.

Пример кода Конкретных Продуктов:


class CoffeeMachineLocator: implements Product {
 let name = “7B Coffee Machine Locator v.3000”
 let image = Image.atPath(“images/locator.tga”)
 let executablePath = “CoffeeMachineLocator.wasm”
}

class iPuchinno: implements Product {
 let name = “iPuchinno 1.0.3”
 let image = Image.atPath(“images/puchino.pvrtc”)
 let executablePath = “neutron/ipuchBugFixFinalNoFreezeFixAlpha4.js”
}

Класс Конкретного Создателя, отдающий массив из двух приложений:


class 7BAppsCreator: implements Creator {
 func factoryMethod() -> [Product] {
  return [CoffeeMachineLocator(), iPuchinno()]
 }
}

После этого компания 7Б компилирует библиотеку Конкретных Продуктов, Конкретного Создателя и совмещает ее с нашим фреймворком, начинает продавать AR очки для своих кофеварок, доработок с нашей стороны не потребуется.

Источники

https://refactoring.guru/ru/design-patterns/command
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612