Рефлексия на примере команд

Мы можем с вами вспомнить класс Router, который берёт на себя ответственность преобразования некоторого запроса в объект команды. С помощью свича мы производим ассоциацию строки (название команды вводимой пользователем) с объектом, который берёт на себя обязанность непосредственного выполнения. Помимо этого, мы также предоставляем команде всё необходимое для выполнения. Так, команда, с помощью конструктора, декларирует, что ей нужно для работы. Часть из того, что ей нужно, передается пользователем через консоль. А часть является частью системы и передается нашим роутером, а пользователь об этом не знает.

В том оформлении, которое есть у нас сейчас, есть ряд недостатков.

Фрагмент 2.33    

public ICommand CreateCommand(Request request)
{
    switch (request.Command)
    {
        case "AddMoney":
            if (request.IsIncorectValuesCount(1)) return null;

            return new AddMoney(_machine, request.Values[0]);
        case "GetChange":
            if (request.IsIncorectValuesCount(0)) return null;

            return new GetChange(_machine);
        case "BuyGood":
            if (request.IsIncorectValuesCount(2)) return null;

            return new BuyGood(_machine, _state.MakeOrder(request));
        case "ShowCommands":
            if (request.IsIncorectValuesCount(0)) return null;

            return new ShowCommands("AddMoney", "GetChange", "BuyGood", "ShowCommands");
        case "Login":
            if (request.IsIncorectValuesCount(0)) return null;

            return new Login(this);
        default:
            return null;
    }
}

Что в этом плохого? На самом деле ничего. В большинстве ситуаций этого будет вполне достаточно. Но в других случаях такое решение может быть не оптимальным. Во-первых, тут дублируется код, во-вторых, добавление новой команды приводит к изменению класса Router.

Мы можем избавиться от этого с помощью рефлексии. Рефлексия – это специальный механизм, который позволяет приложению “смотреть” в себя и работать с типами и их содержим, как с обычными объектами. Что даёт нам возможность, например, найти все классы, которые реализуют интерфейс ICommand, взять из них класс с определенным именем и создать из него объект, а далее вернуть ссылку на него.

Именно это мы и сделаем с вами. Мы не будем затрагивать ничего, кроме класса Router и все изменения будут только в нём (кроме команды ShowCommands).

Базой для нас является возможность динамически найти все типы в приложение, которые удовлетворяют нас.

Фрагмент 2.34    

private readonly Type _commandBaseType = typeof(ICommand);

...

private IEnumerable<Type> GetCommandsTypes()
{
    return AppDomain
        .CurrentDomain
        .GetAssemblies()
        .SelectMany(assembly => assembly.GetTypes())
        .Where(type => _commandBaseType.IsAssignableFrom(type))
        .Where(type => IsRealClass(type));
}

private Type GetCommandTypeByName(string name)
{
    return GetCommandsTypes()
        .Where(type => type.Name == name)
        .FirstOrDefault();
}

private bool IsRealClass(Type testType)
{
    return testType.IsAbstract == false
            && testType.IsGenericTypeDefinition == false
            && testType.IsInterface == false;
}

С помощью метода GetCommandsType, мы получаем все типы, которые реализуют интерфейс ICommand, которые являются не абстрактным и не обобщенным классом.

А с помощью метода GetCommandTypeByName мы уже среди них выбираем только те, имя которых нам подходит. В нашем новом роутере теперь всё работает таким образом, что имя команды, которое вводит пользователь, является прямым указанием на то, какой класс эту команду реализует. Т.е для команды ShowCommands он будет искать класс с этим названием. И очень важно, чтобы этот класс реализовывал интерфейс ICommand.  

Когда у нас есть информация о типе, мы можем сделать много интересных действий. Для нас важно одно – создать объект этого типа. Но чтобы его совершить, нужно решить следующие задачи:

  1. Найти подходящий нам конструктор;
  2. Подготовить аргументы для его вызова;
  3. Вызвать его;

Фрагмент 2.35  

private ICommand CreateInstance(Type type, Request request)
{
    ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public);

    foreach (var ctor in constructors)
    {
        var args = ResolveDependeciesAndMerge(ctor, request);
        if (args != null)
        {
            return (ICommand)ctor.Invoke(args);
        }
    }

    return null;
}

За это у нас ответственен метод CreateInstance, который создает объект команды из заданного типа на основе пришедшего запроса (напоминаю, что в запросе находится данные для команды).

Сначала мы ищем все конструкторы объекта, которые являются публичными. Далее мы перебираем их до тех пор, пока не сможешь найти тот, которому мы сможем дать всё необходимое. Как только мы его находим, мы его вызываем передавая данные для его параметров. В ответ он нам возвращает объект, который возвращаем и мы.

Благодаря этим методам нам удалось сократить метод CreateCommand и сделать роутер динамически подстраиваемым под уже имеющиеся команды. Если мы хотим добавить команду, нам нужно только создать класс и определить в нём публичный конструктор. Всё остальное за нас сделает построенная нами система.

Фрагмент 2.36

public ICommand CreateCommand(Request request)
{
    var commandType = GetCommandTypeByName(request.Command);
    if (commandType != null)
    {
        var instance = CreateInstance(commandType, request);
        return instance;
    }
    else
    {
        return null;
    }
}

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


Leave a Reply

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