Search code examples
c#classinheritancecommandderived-class

How to implement chat commands as separate classes?


I am currently working on the chatbot for the Twitch channel and would like to have all the commands to be separate classes in the program so that I will only need to add or remove a single class to add or remove command.

I've searched through the Internet for quite long but never came across the suitable design. Below is what I think it has to look like but can't find the proper syntax.

class Command()
{
    string keyword;
    int globalCooldown;
    List<string> args;
}

class DoStuffA : Command(List<string> _args)
{
    keyword = "pleasedostuffa";
    globalCooldown = 2;
    args = _args;

    DoStuff(List<string> args)
    {
      //doing stuff A here with all the logic and so on
    }
}
class DoStuffB : Command(List<string> _args)
{
    keyword = "pleasedostuffb";
    globalCooldown = 8;
    args = _args;

    DoStuff(List<string> args)
    {
      //doing stuff B here with all the logic and so on
    }
}

Why do I need this is because I want to store all possible commands in the List<Commands> and when the new chat message appears, search which object of this list matches the keyword with the chat command and execute the appropriate function. For example, if someone posts !pleasedostuffa, I perform

foreach (Command c in commands)//commands is List<Command>
{
    if(c.keyword==receivedCommand.command)//.command is string
    {
        c.DoStuff(receivedCommand.argsAsList)//.argsAsList is List<string>
    }
}

I hope I explained this properly and really am eager to have at least a clue on how could this implemented.

Thank you in advance!


Solution

  • You have the method setup almost right, though there are a few other changes you need. You need to have the base class expose DoStuff() as a virtual method. Try this:

    public abstract class Command
    {
        public string keyword;
        public int globalCooldown;
        //List<string> args;
    
        public abstract void DoStuff(List<string> args);
    }
    
    public class DoStuffA : Command
    {
        //public string keyword = "pleasedostuffa";
        //public int globalCooldown = 2;
        //args = _args;
    
        public DoStuffA()
        {
            keyword = "pleasedostuffa";
            globalCooldown = 2;
        }
    
        public override void DoStuff(List<string> args)
        {
          //doing stuff A here with all the logic and so on
        }
    }
    
    public class DoStuffB : Command
    {
        //public string keyword = "pleasedostuffb";
        //public int globalCooldown = 8;
        // args = _args;
    
        public DoStuffB()
        {
            keyword = "pleasedostuffb";
            globalCooldown = 8;
        }
    
        public override void DoStuff(List<string> args)
        {
          //doing stuff B here with all the logic and so on
        }
    }
    

    So, a couple of notes.

    Method inheritance

    Here I make the base class abstract, simply to enforce that each and every child command implements the abstract function DoStuff(). After all, what would you do with an instance of the base class? It wouldn't do anything, because you don't have an actual implementation. So abstract helps both to avoid accidentally instantiating Command itself and also makes sure sub-type implementers do the right thing.

    Second, at the child class level, you need to override the method on the base class. This ensures that anything calling ((Command)doStuffB).DoStuff() gets the proper implementation of the function.

    Now that you have a DoStuff() method on Command, your foreach loop should work as you expect. You have the method available on the base class, so the virtual overrides at the child level can be run without casting.

    Base class member access

    The fields you are trying to declare here, keyword and globalCooldown, aren't how people would typically expose information like this, but before we get to that I'm going to explain the more fundamental principle of accessing base-class members from the inherited classes.

    These two fields need to be marked public (and given a proper type) so that they can be used from outside the class (in your foreach). The public keyword is called an accessibility modifier, and there are a few other options for accessibility, but in your case only public is likely to do what you want.

    As you can see, I've commented out the fields in the child classes. If you declare them there, they will hide (but not override) the members of the same name on the base class. There's no equivalent of virtual or abstract for fields, so you need another strategy. Here, we leave your original declaration of those fields on the base class so that they are available to anything holding any type of a Command. But instead of redeclaring them at the child class level, we simply set the values of the base class members in the constructor for the child classes.

    Note that for clarity's sake, you could explicitly specify that you are setting a member on the base class by using base.keyword = "etc"; instead.

    Exposing internal values via properties

    As I noted, this will work, but it's not quite how most people would expose the keyword and globalCooldown values. For this, you'd typically use a property instead. This lets you store and expose the values without risking letting someone change the value (intentionally or unintentionally). In this case, you'd want to declare the property this way:

    public string Keyword // properties start with a capital letter by convention
    {
        get; // The get accessor is public, same as the overall property
        protected set; // the set accessor is protected
    }
    

    The protected set accessor means that this is still accessible to be set by the child classes, but not by anyone else. This is probably what you want. So now, in your child constructor, you can set base.Keyword = "whatever"; and your foreach code can reference, but not overwrite, that value. You can declare GlobalCooldown in a similar way.