10
\$\begingroup\$

I've made a pretty standard console in which you type a command and it does something. However, the issue that comes to my mind is scaling: if I want to add hundreds of commands, I have to manually add a new Command instance for each one individually, which is... less than ideal.

The full code is stored in this GitHub repository.

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsolePlus.Commands;

namespace ConsolePlus
{
    public class Program
    {
        /// <summary>
        /// The version of the program.
        /// </summary>
        public const string Version = "1.0.0.0";

        public static CommandRegistry<Command> Registry = new CommandRegistry<Command>();

        /// <summary>
        /// The application's entry point.
        /// </summary>
        /// <param name="args">The command-line arguments passed to the program.</param>
        public static void Main(string[] args)
        {
            CommandHandler.RegisterCommands();

            Console.WriteLine("ConsolePlus v." + Version);
            while (true)
            {
                Console.Write(">>> ");
                string line = Console.ReadLine();
                List<string> parts = line.Split(' ').ToList<string>();
                string commandName = parts[0];
                parts.RemoveAt(0);
                string[] commandArgs = parts.ToArray<string>();

                try
                {
                    string result = Registry.Execute(commandName, commandArgs);
                    if (result != null)
                    {
                        Console.WriteLine("[{0}] {1}", commandName, result);
                    }
                }
                catch (CommandNotFoundException)
                {
                    Console.WriteLine("[ConsolePlus] No such command.");
                }
            }
        }
    }
}

CommandRegistry.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlus
{
    public class CommandRegistry<T>
        where T : ICommand
    {
        private Dictionary<string, T> register;

        public CommandRegistry()
        {
            register = new Dictionary<string, T>();
        }

        public CommandRegistry(params T[] commands)
            : this()
        {
            foreach (T command in commands)
            {
                register.Add(command.Name, command);
            }
        }

        public T GetCommand(string name)
        {
            if (register.ContainsKey(name))
            {
                return register[name];
            }
            else
            {
                throw new CommandNotFoundException(name);
            }
        }

        public string Execute(string name, string[] args)
        {
            if (register.ContainsKey(name))
            {
                return register[name].Execute(args);
            }
            else
            {
                throw new CommandNotFoundException(name);
            }
        }

        public void RegisterCommand(T command)
        {
            register.Add(command.Name, command);
        }
    }
}

ICommand.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlus
{
    public interface ICommand
    {
        string Name { get; set; }

        string HelpText { get; set; }

        bool IsPrivileged { get; set; }

        string Execute(string[] args);
    }
}

Command.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlus
{
    public class Command : ICommand
    {
        public string Name { get; set; }

        public string HelpText { get; set; }

        public bool IsPrivileged { get; set; }

        private Func<string[], string> method;

        public Command(string name, bool privileged, string help, Func<string[], string> commandMethod)
        {
            Name = name;
            IsPrivileged = privileged;
            HelpText = help;
            method = commandMethod;
        }

        public string Execute(string[] args)
        {
            return method.Invoke(args);
        }
    }
}

CommandHandler.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlus.Commands
{
    public class CommandHandler
    {
        public static List<Command> AllCommands = new List<Command>
        {
            new Command("clear", false, ClearCommand.HelpText, ClearCommand.CommandMethod),
            new Command("exit", false, ExitCommand.HelpText, ExitCommand.CommandMethod)
        };

        public static void RegisterCommands()
        {
            foreach (Command command in AllCommands)
            {
                Program.Registry.RegisterCommand(command);
            }
        }
    }
}

And just for illustration (the other *Command classes are very similar):

ClearCommand.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsolePlus.Commands
{
    class ClearCommand
    {
        public static string HelpText = "Clears the console screen.";

        public static string CommandMethod(string[] args)
        {
            Console.Clear();
            return null;
        }
    }
}

At the moment, to add another command, I have to create another class and add another new Command line to the CommandHandler.RegisterCommands method - not ideal. Reviews of this specifically, and if there are any better ways to do this, would be very useful - though of course, any review is a good thing.

\$\endgroup\$

1 Answer 1

17
\$\begingroup\$

Why do you have the Command class? Why not make life much simpler for yourself by making AllCommands a List<ICommand>, and having classes like ClearCommand implement ICommand?

You can then query your assembly file for classes where ICommand is implemented; that way you don't even need to fill AllCommands "by hand":

AllCommands = Assembly.GetExecutingAssembly().GetTypes()
    .Where(x => x.GetInterfaces().Contains(typeof(ICommand))
                && x.GetConstructor(Type.EmptyTypes) != null)
    .Select(x => Activator.CreateInstance(x) as ICommand);

Also, does AllCommands need to be called AllCommands? Can't it just be called Commands?

I would also recommend to remove the setters from the properties in ICommand. That way you'd end up with something like this:

public interface ICommand
{
    string Name { get; }

    string HelpText { get; }

    bool IsPrivileged { get; }

    string Execute(string[] args);
}

public class ClearCommand : ICommand
{
    public string Name { get { return "clear"; } }

    public string HelpText { get { return "Clears the console screen."; } }

    public bool IsPrivileged { get { return false; } }

    public string Execute(string[] args)
    {
        Console.Clear();
        return null;
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Reading an answer like this makes me hyped to write C#! \$\endgroup\$ Sep 25, 2015 at 17:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.