Классификация и обработка результата плавающего типа в Java

Пишу бота для Discord на JDA. Хочу всё структурировать, классифицировать по красоте, читаемости и расширяемости с учётом принятых в Java стандартов оформления кода (в которых мало что понимаю, поэтому и задаю этот вопрос).

Схема структуры программы, псевдокод

public class Bot {

    private Commands activeCommands;

    public static void main(String[] args) {

        // ...

        Bot bot = new Bot(token);
    }

    public Bot(String token) {

        activeCommands = new Commands();
        activeCommands.add(new HelpCommand());
        activeCommands.add(new PingCommand());
        activeCommands.add(new DiceCommand());
        activeCommands.add(new BlackJackCommand());
        // etc...

        JDA jda = new JDABuilder(token)
            .addEventListeners(new BotListenerAdapter(activeCommands))
            .build();
    }
}

public class Commands extends HashSet<Command> {
    // ...
}

public abstract class Command {

    protected Set<String> aliases;

    public Command(String name) {
        this.aliases = new HashSet<>();
        this.aliases.add(name);
    }

    public Command(String[] aliases) {
        this.aliases = new HashSet<>(Arrays.asList(aliases));
    }

    abstract protected ??? action(String[] params);
}

public class BotListenerAdapter extends ListenerAdapter {

    private Commands activeCommands;

    public BotListenerAdapter(Commands cmds) {
        activeCommands = cmds;
    }

    @Override
    public void onMessageReceived(@Nonnull MessageReceivedEvent event) {

        Message msg = event.getMessage();
        MessageChannel channel = msg.getChannel();
        String content = msg.getContentRaw();

        // кусок кода, проверяющий, является ли content командой
        // и разделяющий его на commandName и commandParams

        Command cmd = activeCommands.find(commandName);
        ??? result = cmd.action(commandParams);

        // далее делаем что-то с результатом, в зависимости от его
        // типа и отправляем ответ через
        channel.sendMessage(...);
        // или
        channel.sendFile(...);
        // или ещё что-то, опять же, в зависимости от типа
    }
}

У меня есть главный класс Bot, в котором объявлено поле private Commands activeCommands, оно передаётся объекту класса BotListenerAdapter extends net.dv8tion.jda.api.hooks.ListenerAdapter, который передаётся JDABuilder'у при создании объекта JDA. Commands — это просто обёрточка над HashSet<Command> с дополнительным функционалом для быстрого поиска нужной команды и всяким таким. Command — это абстрактный класс, от которого наследуются разные команды и реализуют соответствующую логику.

По задумке BotListenerAdapter обрабатывает получение сообщения в канале через переопределение метода onMessageReceived, проверяет наличие команды в activeCommands и вызывает у неё метод action (тот самый абстрактный метод с логикой), передавая параметры команды, после чего метод action отрабатывает и возвращает некий результат, который там же в обработчике onMessageReceived бот должен отправить ответом в канал дискорда в том или ином формате. Ну, чтобы не передавать в объект команды всякую муть, вроде объектов канала, отправителя и т.д. Команда отработала автономно, а ответом занимается уже BotListenerAdapter, получив её результат.

Проблема вот в чём: результат исполнения команды может быть разный. Это может быть как plain-text-ответ или Discord Embed или какая-нибудь гифка, так и более сложная конструкция, напичканная всякими фьючерами (или потоками, пока не знаю, пишу «сверху-вниз»), представляющая из себя Embed с функциональными кнопками, реализованными через reactions дискорда. Естественно, разные виды результата и обрабатывать нужно по-разному и представлять пользователю в разном формате и разными способами. Как быть в таком случае? Каким образом лучше всего будет классифицировать результат исполнения Command.action и как отличить один вид результата от другого?

Было две идеи. Первая:

Написать inferface CommandResult с методом process, принимающим в качестве параметров всё, что может понадобиться для обработки результата, в т.ч. объекты канала, пользователя, отправившего команду, список активных в текущий момент игр (бот игровой), контекст базы данных и прочее-прочее, и реализовывать через него разные подклассы результатов. Совсем не кажется хорошим решением, т.к. тащить всю эту вереницу ссылок на глубокие уровни не хочется. Тем более, дальнейшая расширяемость от этого явно пострадает, ведь Бог его знает, какой ещё функционал потребуется добавить. Слишком много придётся дописывать и переписывать.

И вторая идея — написать enum CommandResult с несколькими константами, определяющими разные виды результатов и эксклюзивными данными и методами для разных констант, но... как-то слегка отдаёт это костылём. Может я ошибаюсь, но есть ощущение, будто бы перечисления в Java для этого никто не использует и должен быть более элегантный подход.

И так, вопросы:

  1. Допустима ли вообще описанная мной структура приложения в Java? Если нет, то что именно в ней плохо? Понятно, что писать можно, как угодно, лишь бы работало, но есть же какие-то негласные стандарты. На одном языке принято делать так, на другом сяк. Я не слишком опытен в Java. Работал с ней немного, когда учился в универе, кое-что понимаю, но это мой первый относительно серьёзный проект на ней.
  2. Приемлем ли какой-то из двух описанных мной вариантов решения данной проблемы? Если да, то какой? Я вполне могу ошибаться по поводу enum и, возможно, это действительно хороший вариант, но мне интересно мнение опытного Java-программиста.
  3. Если оба предложенных мной варианта действительно неприемлемы, то как лучше сделать? Как бы сделали Вы?

Ответы (1 шт):

Автор решения: tym32167

В Java допустима любая архитектура, которая компилируется. Нет каких то специфичных для Java особых архитектур.

Вопрос номер раз: зачем их команды что то возвращать? Не может ли сама команда печатать вывод куда надо? Смотрим паттерн команда.

Пример команды:

abstract class Command {
    public abstract boolean canExecute(Object obj);
    public abstract void execute(Object obj);
}

class CurrentTimeCommand extends Command {

    private final PrintStream printStream;

    public CurrentTimeCommand(PrintStream printStream){
        this.printStream = printStream;
    }

    @Override
    public boolean canExecute(Object obj) {
        return  ("timenow".equals(obj));
    }

    @Override
    public void execute(Object obj) {
        this.printStream.println("Time is " + LocalDateTime.now());
    }
}

Использование

public static void main(String[] args) {
    ArrayList<Command> commands = new ArrayList<>();
    commands.add(new CurrentTimeCommand(System.out));

    Scanner scanner = new Scanner(System.in);
    String inputText = scanner.nextLine();

    commands.stream().filter(cmd->cmd.canExecute(inputText)).forEach(cmd->cmd.execute(inputText));
}

Другой вариант называется цепочка обязанностей

Суть в том, что можно передавать один и тот же мессадж командам. Например, вот у нас есть наша базовая команда

abstract class Command {
    public abstract boolean canExecute(Object obj);
    public abstract void execute(Object obj);
}

Но в этот раз у нас есть еще и обработчик

class CommandHandler {
    ArrayList<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    public <T> void handle(T input) {
        commands.stream().filter(cmd -> cmd.canExecute(input)).forEach(cmd -> cmd.execute(input));
    }
}

Сделаем отдельную команду на вывод в печать, вот её входной параметр

class PrintInput {
    private final String content;

    public PrintInput(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

Сама команда

class PrintCommand extends Command {

    private final PrintStream printStream;

    public PrintCommand(PrintStream printStream) {
        this.printStream = printStream;
    }

    @Override
    public boolean canExecute(Object obj) {
        return obj.getClass().equals(PrintInput.class);
    }

    @Override
    public void execute(Object obj) {
        PrintInput input = (PrintInput) obj;
        this.printStream.println(input.getContent());
    }
}

Тогда наша команда с текущим временем будет такая

class CurrentTimeCommand extends Command {

    private final CommandHandler commandHandler;

    public CurrentTimeCommand(CommandHandler commandHandler) {
        this.commandHandler = commandHandler;
    }

    @Override
    public boolean canExecute(Object obj) {
        return ("timenow".equals(obj));
    }

    @Override
    public void execute(Object obj) {
        String payload = "Time is " + LocalDateTime.now();
        commandHandler.handle(new PrintInput(payload));
    }
}

Соберем все вместе

    CommandHandler commandHandler = new CommandHandler();
    commandHandler.addCommand(new CurrentTimeCommand(commandHandler));
    commandHandler.addCommand(new PrintCommand(System.out));

    Scanner scanner = new Scanner(System.in);
    String inputText = scanner.nextLine();

    commandHandler.handle(inputText);

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


Если развивать тему дальше, то можно уйти совсем в дебри и вспоминить Бертрана Майера, который предлагал все методы делить на команды и запросы (CQRS, Фаулер), но это уже ближе к теме микросервисов и DDD.

→ Ссылка