javadiscord / java-discord-api

A wrapper over the discord API to create bots using Java
GNU General Public License v3.0
7 stars 8 forks source link

EventListeners with Constructors #149

Closed Taz03 closed 1 month ago

Taz03 commented 1 month ago

Right now if a class annotated with @EventListener has a non no-args constructor it is not considered a valid EventListener.

@EventListener
public class EchoHandler {
    private String database;

    public EchoHandler(String database) {
        this.database = database;
    }

    @MessageCreate
    public void echoMessages(Message message, Guild guild) {
        if (!message.author().bot()) {
            // write message to database

            guild.channel().createMessage(
                new CreateMessageBuilder(message.channelId()).content(message.content())
            );
        }
    }
}

A possible solution would be to integrate it with a DI framework, and automatically initialize all classes annotated with EveneListener with Autowired params

surajkumar commented 1 month ago

What about a different approach? Doing something like autowired is crazy complex and we're really deep into our own code to now swap it for a DI framework (those are very very big).

What about a more pragmatic approach?

E.g. we can have a global static map that you can store instances of objects inside e.g.

static Map<Class, Object> objectsMap;

Then inside the listener, we can do

MyClass instance = (MyClass) objectsMap.get(MyClass.class);

Or if we know a way, we can use an Annotation to inject the instance?

surajkumar commented 1 month ago

@Taz03 here I added this to my PR: https://github.com/javadiscord/java-discord-api/pull/148/commits/c1acab7c548b3baf2e207e8aa5f6e056d4274ced

The way it works is you create any static builder for example:

    @Component
    public static ChatGPT chatGpt() {
        return new ChatGPT();
    }

Add this anywhere into project and make sure it's static! If it's not static, we have to initalize the parent class. That means we would have to either tell the user to have a 0-arg constructor or we figure out the parameters. Not good. Using static makes it really simple.

Next, we can reference this component anywere in classes that use @SlashCommand or @EventListener.

Like this:

    @Inject
    private ChatGPT chatGPT;
SquidXTV commented 1 month ago

@Taz03 here I added this to my PR: c1acab7

The way it works is you create any static builder for example:

    @Component
    public static ChatGPT chatGpt() {
        return new ChatGPT();
    }

Add this anywhere into project and make sure it's static! If it's not static, we have to initalize the parent class. That means we would have to either tell the user to have a 0-arg constructor or we figure out the parameters. Not good. Using static makes it really simple.

Next, we can reference this component anywere in classes that use @SlashCommand or @EventListener.

Like this:

    @Inject
    private ChatGPT chatGPT;

What if this component like chatgpt in this case needs some other component or data in general? How do you provide it?

surajkumar commented 1 month ago

@Taz03 here I added this to my PR: c1acab7 The way it works is you create any static builder for example:

    @Component
    public static ChatGPT chatGpt() {
        return new ChatGPT();
    }

Add this anywhere into project and make sure it's static! If it's not static, we have to initalize the parent class. That means we would have to either tell the user to have a 0-arg constructor or we figure out the parameters. Not good. Using static makes it really simple. Next, we can reference this component anywere in classes that use @SlashCommand or @EventListener. Like this:

    @Inject
    private ChatGPT chatGPT;

What if this component like chatgpt in this case needs some other component or data in general? How do you provide it?

The method annotated with @Component is just a static getter.

Currently, the way it works is, the codebase scans for all @Component in the class path. It will then invoke it so we can have the instance.

Next, when we load the @EventListener and @SlashCommand, after we create the object, we inject any @Component objects we created into the fields that have @Inject

and that's all good to go.

The problem that is being solved is @EventListener having a requirement of 0 arg-constructor. We can additionally, use @Inject on a constructor too if this is something we want implemented.

SquidXTV commented 1 month ago

The method annotated with @Component is just a static getter.

Currently, the way it works is, the codebase scans for all @Component in the class path. It will then invoke it so we can have the instance.

Next, when we load the @EventListener and @SlashCommand, after we create the object, we inject any @Component objects we created into the fields that have @Inject

and that's all good to go.

The problem that is being solved is @EventListener having a requirement of 0 arg-constructor. We can additionally, use @Inject on a constructor too if this is something we want implemented.

I doubt that answers my question. What if the component itself got dependencies. Using the chatgpt component as an example: Imagine you want to load the key from a config file and then use it in the chatgpt constructor itself. How are you going to do that?

surajkumar commented 1 month ago

The method annotated with @Component is just a static getter. Currently, the way it works is, the codebase scans for all @Component in the class path. It will then invoke it so we can have the instance. Next, when we load the @EventListener and @SlashCommand, after we create the object, we inject any @Component objects we created into the fields that have @Inject and that's all good to go. The problem that is being solved is @EventListener having a requirement of 0 arg-constructor. We can additionally, use @Inject on a constructor too if this is something we want implemented.

I doubt that answers my question. What if the component itself got dependencies. Using the chatgpt component as an example: Imagine you want to load the key from a config file and then use it in the chatgpt constructor itself. How are you going to do that?

I see the misunderstanding, so the point of the @Component being part of a static method is because we then do not need to instaniate the object ourselves. Here's an example:

public class Main {
  private static ChatGpt chatGpt;

  public static void main(String[] args) {
    chatGpt = new ChatGpt(args);
  }

  @Component
  public static ChatGpt chatgpt() {
    return chatgpt;
  }
}

In this example, the ChatGpt object is being created by the user (in my example, the main method).

This is how I would expect other people to setup their components, they can do it anywhere and in any class. As long as all the objects are ready and created before they call discord.start().

We have to remember, we are not creating a general purpose DI framework. We are supporting a very specific usecase which is just passing an instance to our @EventListener and @SlashCommand classes. If the end user wants to do more than that then they should use a DI framework of their choice.

The only annotations we support are @Inject and @Component. We have no further need to be able to provide more flexibility. There just isn't a requirement for this or a real usecase for it when it comes to this discord framework.

Some DI frameworks have annotations like @Factory - we don't need this! Our @EventListeners are singleton. Since we create the entire lifecycle of these objects as they are our own. Our framework creates and manages them. They are only entry points to discord events for the user to write their code inside.

Can we support further functionality? Of course, if we want to add @Inject on constructors for example, we deffo can, it won't take more than 5-10mins to implement.

If we don't want singleton Components we don't have to but we first need to identify the problem before we implement a soluton.

Once again, remember, we are not creating a general purpose DI framework and any extra functionality that a user might want or need, they can introduce on their own by adding the relevant libraries to their builds.