Наделяем команды описанием через атрибуты

На данный момент мы используем имя типа, который представляет команду как некий ключ, по которому мы можем её найти. Если пользователь вводит команду AddMoney, то мы буквально ищем одноименный класс. Это не всегда удобно и нам хотелось бы наделить команду некоторой описательной информацией.

Чего хочется добится:

  1. Возможность давать команде имя;
  2. Проверять на этапе компиляции что нет двух команд с одинаковым именем;

Один из возможных путей решение состоит в том, чтобы расширить интерфейс ICommand и добавить в него метод или свойство GetName. Этот вариант интересен и даже весь жизнеспособен. Но не в нашей ситуации. Во-первых, команду описывает тип, а не объект. Так что мы не можем говорить, что имя является частью объекта. Во-вторых, это затруднит статический анализ. В-третьих, не всегда подобные вещи являются частью абстракции ICommand.

Сейчас я хочу описать решение через атрибуты. Атрибуты – это специальных механизм C#, который позволяет нам добавлять декларативное описание  к различным сущностям. И впоследствии пользоваться этим описанием через механизм рефлексии.

Мы можем создавать как свои атрибуты, так и использовать уже готовые. Яркий пример атрибут Serializable, который позволяет пометить необходимость сериализации того или иного члена. Я хочу создать атрибут Command, который позволит мне добавить описание класса. В нашем случае – название команды.

Начнём с создания атрибута.

Листинг WendingMachine/CommandsSystem/Attributes.cs

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
class CommandAttribute : Attribute
{
    public string CommandName;

    public CommandAttribute(string commandName)
    {
        CommandName = commandName;
    }
}

Нам нужно определить класс CommandAttribute, который наследуется от Attribute. А также, нужно пометить получившийся класс атрибутом AttributeUsage, в котором мы указываем: к чему мы можем применять получившийся атрибут, можно ли применять его несколько раз и наследуется ли он. В нашем случае я указал, что один класс может быть связан с несколькими команда и то, что производные классы не являются связанными с этими же командами.

Теперь рассмотрим применение этого. Во-первых, вот так мы можем пометить класс.

Листинг WendingMachine/CommandsSystem/Commands/AddMoney.cs

[Command("AddMoney")]
class AddMoney : ICommand
{
    private WendingMachine _machine;
    private int _money;

    public AddMoney(WendingMachine machine, int money)
    {
        _machine = machine;
        _money = money;
    }

    public void Execute()
    {
        _machine.AddBalance(_money);
    }
}

Заметьте, что прямо над классом указывается наш атрибут. Во-вторых, мы можем получить информацию об атрибутах через рефлексию в момент поиска нужной команды.

Фрагмент 2.43

private Type GetCommandTypeByName(string name)
{
    return GetCommandsTypes()
        .Where(type => type.GetCustomAttributes<CommandAttribute>()
                        .Any(attribute => attribute.CommandName == name))
        .FirstOrDefault();
}

Нам пришлось изменить только метод GetCommandTypeByName. В отличие от старой версии, в Where, мы выбираем только те типы, у которых есть атрибут CommandAttribute со значением CommandName, равным названию искомой команды. Попутно нам нужно внести правку в метод GetCommands

Фрагмент 2.44

public IEnumerable<string> GetCommands()
{
    return GetCommandsTypes()
            .SelectMany(type => type.GetCustomAttributes<CommandAttribute>())
            .Select(attribute => attribute.CommandName);
}

Если вы нашли ошибку, пожалуйста выделите её и нажмите Ctrl+Enter.


Leave a Reply

Your email address will not be published. Required fields are marked *