В конце у меня есть конкретный вопрос, но я хочу предоставить множество предпосылок и контекста, чтобы вы как можно больше находились на одной странице и могли понять мою цель.
Фон
Я создаю консольное приложение с ASP.NET MVC 3. Оно находится по адресу http://www.u413.com, если вам нужно пойти туда и получить представление о том, как оно работает. Сама концепция проста: получить командные строки от клиента, проверить, существует ли предоставленная команда, и действительны ли аргументы, предоставленные в команде, выполнить команду и вернуть набор результатов.
Внутренние выработки
С помощью этого приложения я решил стать немного креативнее. Наиболее очевидным решением для приложения в стиле терминала является создание крупнейшего в мире оператора IF. Запустите каждую команду через оператор IF и вызовите соответствующие функции изнутри. Мне не понравилась эта идея. В более старой версии приложения это было так, как оно работало, и это был огромный беспорядок. Добавление функциональности в приложение было до смешного трудным.
После долгих раздумий я решил создать специальный объект, называемый командным модулем. Идея состоит в том, чтобы создать этот командный модуль с каждым запросом. Объект модуля будет содержать все доступные команды в качестве методов, и сайт будет затем использовать отражение, чтобы проверить, соответствует ли команда, предоставленная пользователем, имени метода. Объект командного модуля находится за интерфейсом с именем ICommandModule
, показанным ниже.
namespace U413.Business.Interfaces
{
/// <summary>
/// All command modules must ultimately inherit from ICommandModule.
/// </summary>
public interface ICommandModule
{
/// <summary>
/// The method that will locate and execute a given command and pass in all relevant arguments.
/// </summary>
/// <param name="command">The command to locate and execute.</param>
/// <param name="args">A list of relevant arguments.</param>
/// <param name="commandContext">The current command context.</param>
/// <param name="controller">The current controller.</param>
/// <returns>A result object to be passed back tot he client.</returns>
object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller);
}
}
Метод InvokeCommand()
- это единственный метод в командном модуле, о котором мой контроллер MVC сразу узнает. В этом случае ответственность этого метода состоит в том, чтобы использовать рефлексию, смотреть на свой экземпляр и находить все доступные методы команд.
Я использую Ninject для внедрения зависимостей. Мой контроллер MVC имеет зависимость конструктора от ICommandModule
. Я создал собственный провайдер Ninject, который создает этот командный модуль при разрешении зависимости ICommandModule
. Существует 4 типа командных модулей, которые может собрать Ninject:
VisitorCommandModule
UserCommandModule
ModeratorCommandModule
AdministratorCommandModule
Существует еще один класс BaseCommandModule
, от которого наследуются все остальные классы модулей. Очень быстро, вот отношения наследования:
BaseCommandModule : ICommandModule
VisitorCommandModule : BaseCommandModule
UserCommandModule : BaseCommandModule
ModeratorCommandModule : UserCommandModule
AdministratorCommandModule : ModeratorCommandModule
Надеюсь, вы уже видите, как это построено. В зависимости от статуса членства пользователя (не авторизован, обычный пользователь, модератор и т. Д.) Ninject предоставит надлежащему командному модулю только те методы команд, к которым у пользователя должен быть доступ.
Все это прекрасно работает. Моя дилемма возникает, когда я анализирую командную строку и выясняю, как структурировать методы команд на объекте командного модуля.
Вопрос
Как анализировать и выполнять командную строку?
Текущее решение
В настоящее время я разбиваю командную строку (строку, переданную пользователем, содержащую команду и все аргументы) в контроллере MVC. Затем я вызываю InvokeCommand()
метод для моего введенного ICommandModule
и передаю команду string
и List<string>
аргументы.
Допустим, у меня есть следующая команда:
TOPIC <id> [page #] [reply “reply”]
Эта строка определяет команду TOPIC, принимающую требуемый идентификационный номер, необязательный номер страницы и необязательную команду ответа со значением ответа.
В настоящее время я реализую метод команды следующим образом (атрибуты выше метода предназначены для получения информации о меню справки. Команда HELP использует отражение, чтобы прочитать все это и отобразить организованное меню справки):
/// <summary>
/// Shows a topic and all replies to that topic.
/// </summary>
/// <param name="args">A string list of user-supplied arguments.</param>
[CommandInfo("Displays a topic and its replies.")]
[CommandArgInfo(Name="ID", Description="Specify topic ID to display the topic and all associated replies.", RequiredArgument=true)]
[CommandArgInfo(Name="REPLY \"reply\"", Description="Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", RequiredArgument=false)]
public void TOPIC(List<string> args)
{
if ((args.Count == 1) && (args[0].IsInt64()))
TOPIC_Execute(args); // View the topic.
else if ((args.Count == 2) && (args[0].IsInt64()))
if (args[1].ToLower() == "reply")
TOPIC_ReplyPrompt(args); // Prompt user to input reply content.
else
_result.DisplayArray.Add("Subcommand Not Found");
else if ((args.Count >= 3) && (args[0].IsInt64()))
if (args[1].ToLower() == "reply")
TOPIC_ReplyExecute(args); // Post user's reply to the topic.
else
_result.DisplayArray.Add("Subcommand Not Found");
else
_result.DisplayArray.Add("Subcommand Not Found");
}
Моя текущая реализация - огромный беспорядок. Я хотел избежать гигантских выражений IF, но все, что я делал, это обменивал одно гигантское выражение IF для всех команд на тонну чуть менее гигантских операторов IF для каждой команды и ее аргументов. Это даже не половина этого; Я упростил эту команду для этого вопроса. В реальной реализации есть еще несколько аргументов, которые могут быть предоставлены с помощью этой команды, и этот оператор IF - самая уродливая вещь, которую я когда-либо видел. Это очень избыточно и совсем не СУХОЙ (не повторяйся), поскольку мне нужно отобразить «Подкоманда не найдена» в трех разных местах.
Достаточно сказать, что мне нужно лучшее решение, чем это.
Идеальная реализация
В идеале я хотел бы структурировать мои командные методы примерно так:
public void TOPIC(int Id, int? page)
{
// Display topic to user, at specific page number if supplied.
}
public void TOPIC(int Id, string reply)
{
if (reply == null)
{
// prompt user for reply text.
}
else
{
// Add reply to topic.
}
}
Тогда я бы с удовольствием это сделал:
- Получение командной строки от клиента.
- Передать командную строку непосредственно в
InvokeCommand()
в ICommandModule
.
InvokeCommand()
выполняет некоторый магический анализ и рефлексию для выбора правильного метода команды с правильными аргументами и вызывает этот метод, передавая только необходимые аргументы.
Дилемма с идеальным воплощением
Я не уверен, как структурировать эту логику. Я почесал голову в течение нескольких дней. Я хотел бы иметь вторую пару глаз, чтобы помочь мне в этом (следовательно, наконец, прибегнуть к роману SO вопрос). В каком порядке все должно происходить?
Должен ли я вытащить команду, найти все методы с этим именем команды, затем перебрать все возможные аргументы, а затем перебрать аргументы моей командной строки? Как мне определить, что идет, куда и какие аргументы идут парами. Например, если я перебираю свою командную строку и нахожу Reply "reply"
, как мне связать содержимое ответа с переменной ответа, при этом встречая число <ID>
и предоставляя его для аргумента Id
?
Я уверен, что сейчас запутался в тебе. Позвольте мне проиллюстрировать несколько примеров командных строк, которые пользователь может передать:
TOPIC 36 reply // Should prompt the user to enter reply text.
TOPIC 36 reply "Hey guys what's up?" // Should post a reply to the topic.
TOPIC 36 // Should display page 1 of the topic.
TOPIC 36 page 4 // Should display page 4 of the topic.
Как мне узнать, чтобы отправить 36 в параметр Id
? Как я знаю, чтобы пара ответила "Эй, ребята, что случилось?" и передать "Эй, ребята, что случилось?" в качестве значения аргумента ответа метода?
Чтобы узнать, какую перегрузку метода вызывать, мне нужно знать, сколько аргументов было предоставлено, чтобы я мог сопоставить это число с перегрузкой метода команды, который принимает то же количество аргументов. Проблема в том, что `TOPIC 36 'ответит:" Эй, ребята, как дела? " на самом деле два аргумента, а не три в качестве ответа и «Эй, ребята ...» идут вместе как один аргумент.
Я не возражаю против раздувания метода InvokeCommand()
немного (или много), пока это означает, что вся сложная ерунда анализа и отражения обрабатывается там, и мои командные методы могут оставаться красивыми и чистыми, и их легко написать .
Полагаю, я просто хочу кое-что понять. У кого-нибудь есть креативные идеи для решения этой проблемы? Это действительно большая проблема, потому что операторы аргумента IF в настоящее время сильно усложняют написание новых команд для приложения. Команды - это та часть приложения, которую я хочу сделать очень простой, чтобы их можно было легко расширять и обновлять. Вот как выглядит метод команды TOPIC в моем приложении:
/// <summary>
/// Shows a topic and all replies to that topic.
/// </summary>
/// <param name="args">A string list of user-supplied arguments.</param>
[CommandInfo("Displays a topic and its replies.")]
[CommandArgInfo("ID", "Specify topic ID to display the topic and all associated replies.", true, 0)]
[CommandArgInfo("Page#/REPLY/EDIT/DELETE [Reply ID]", "Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", false, 1)]
public void TOPIC(List<string> args)
{
if ((args.Count == 1) && (args[0].IsLong()))
TOPIC_Execute(args);
else if ((args.Count == 2) && (args[0].IsLong()))
if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyPrompt(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditPrompt(args);
else if (args[1].ToLower() == "delete")
TOPIC_DeletePrompt(args);
else
TOPIC_Execute(args);
else if ((args.Count == 3) && (args[0].IsLong()))
if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
TOPIC_EditReplyPrompt(args);
else if ((args[1].ToLower() == "delete") && (args[2].IsLong()))
TOPIC_DeleteReply(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditExecute(args);
else if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyExecute(args);
else if (args[1].ToLower() == "delete")
TOPIC_DeleteExecute(args);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
else if ((args.Count >= 3) && (args[0].IsLong()))
if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyExecute(args);
else if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
TOPIC_EditReplyExecute(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditExecute(args);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
}
Разве это не смешно? У каждой команды есть такой монстр, и это недопустимо. Я просто перебираю сценарии в моей голове и то, как код может справиться с этим. Я очень гордился настройкой моего командного модуля, теперь, если бы я мог гордиться реализацией командного метода.
Хотя я не собираюсь прыгать с моей всей моделью (командными модулями) для приложения, я определенно открыт для предложений. В основном меня интересуют предложения, касающиеся разбора строки командной строки и сопоставления ее аргументов с правильными перегрузками методов. Я уверен, что любое решение, которое я выберу, потребует значительного количества редизайна, поэтому не бойтесь предлагать что-то, что вы считаете ценным, даже если я не обязательно использую ваше предложение, оно может поставить меня на правильный путь.
Редактировать: сделать длинный вопрос длиннее
Я просто хотел очень быстро прояснить, что сопоставление команд с методами команд на самом деле меня не беспокоит. Меня больше всего волнует, как анализировать и организовывать строку командной строки. В настоящее время метод InvokeCommand()
использует несколько очень простых C # отражений для поиска подходящих методов:
/// <summary>
/// Invokes the specified command method and passes it a list of user-supplied arguments.
/// </summary>
/// <param name="command">The name of the command to be executed.</param>
/// <param name="args">A string list of user-supplied arguments.</param>
/// <param name="commandContext">The current command context.</param>
/// <param name="controller">The current controller.</param>
/// <returns>The modified result object to be sent to the client.</returns>
public object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller)
{
_result.CurrentContext = commandContext;
_controller = controller;
MethodInfo commandModuleMethods = this.GetType().GetMethod(command.ToUpper());
if (commandModuleMethods != null)
{
commandModuleMethods.Invoke(this, new object[] { args });
return _result;
}
else
return null;
}
Итак, как вы можете видеть, меня не волнует, как найти методы команд, так как это уже работает. Я просто размышляю над хорошим способом разбора командной строки, организации аргументов и последующего использования этой информации для выбора правильного метода команды / перегрузки с использованием отражения.
ОБНОВЛЕНИЕ BOUNTY
Я получил награду за этот вопрос. Я ищу действительно хороший способ разбора командной строки, которую я передаю. Я хочу, чтобы синтаксический анализатор идентифицировал несколько вещей:
- Опции. Определите параметры в командной строке.
- Имя / Значение Пары. Определите пары имя / значение (например, [page #] <- включает ключевое слово "page" и значение "#") </li>
- Значение только. Определить только значение.
Я хочу, чтобы они определялись с помощью метаданных при первой перегрузке метода команды. Вот список примеров методов, которые я хочу написать, украшенный метаданными, которые будут использоваться синтаксическим анализатором при отражении. Я дам вам эти примеры методов и некоторые примеры командных строк, которые должны соответствовать этому методу. Тогда я оставлю это на ваше усмотрение ТАК, чтобы вы нашли хорошее решение для парсера.
// Metadata to be used by the HELP command when displaying HELP menu, and by the
// command string parser when deciding what types of arguments to look for in the
// string. I want to place these above the first overload of a command method.
// I don't want to do an attribute on each argument as some arguments get passed
// into multiple overloads, so instead the attribute just has a name property
// that is set to the name of the argument. Same name the user should type as well
// when supplying a name/value pair argument (e.g. Page 3).
[CommandInfo("Test command tests things.")]
[ArgInfo(
Name="ID",
Description="The ID of the topic.",
ArgType=ArgType.ValueOnly,
Optional=false
)]
[ArgInfo(
Name="PAGE",
Description="The page number of the topic.",
ArgType=ArgType.NameValuePair,
Optional=true
)]
[ArgInfo(
Name="REPLY",
Description="Context shortcut to execute a reply.",
ArgType=ArgType.NameValuePair,
Optional=true
)]
[ArgInfo(
Name="OPTIONS",
Description="One or more options.",
ArgType=ArgType.MultiOption,
Optional=true
PossibleValues=
{
{ "-S", "Sort by page" },
{ "-R", "Refresh page" },
{ "-F", "Follow topic." }
}
)]
[ArgInfo(
Name="SUBCOMMAND",
Description="One of several possible subcommands.",
ArgType=ArgType.SingleOption,
Optional=true
PossibleValues=
{
{ "NEXT", "Advance current page by one." },
{ "PREV", "Go back a page." },
{ "FIRST", "Go to first page." },
{ "LAST", "Go to last page." }
}
)]
public void TOPIC(int id)
{
// Example Command String: "TOPIC 13"
}
public void TOPIC(int id, int page)
{
// Example Command String: "TOPIC 13 page 2"
}
public void TOPIC(int id, string reply)
{
// Example Command String: TOPIC 13 reply "reply"
// Just a shortcut argument to another command.
// Executes actual reply command.
REPLY(id, reply, { "-T" });
}
public void TOPIC(int id, List<string> options)
{
// options collection should contain a list of supplied options
Example Command String: "TOPIC 13 -S",
"TOPIC 13 -S -R",
"TOPIC 13 -R -S -F",
etc...
}
Синтаксический анализатор должен принять командную строку, использовать отражение, чтобы найти все возможные перегрузки метода команды, использовать отражение, чтобы прочитать атрибуты аргумента, чтобы помочь определить, как разделить строку на правильный список аргументов, а затем вызвать соответствующую команду. перегрузка метода, передача правильных аргументов.