Violet Tape некоторые мысли о разработке на платформе .Net

28Сен/101

Command Manager

Сегодня я хочу рассказать про шаблон Команда и Менеджер команд. Все к этому плавно и шло. В прошлой статье это так и напрашивалось, но мне было лень это туда вкручивать, решил рассказать отдельно. Хотя сначала у меня были сомнения в том, как это рассказывать и нужно ли вообще. Потому что я с ходу накидал 5 способов реализации (по мере усложнения и развития идеи, но потом один все же забраковал) и этот функционал в еще большей степени развит и встроен в WPF. В итоге я думаю это будет полезно новичкам, чтобы было лучше понятно как это использовать в дальнейшем, в WPF, да и вообще.

Начнем с официальной части, вот что говорит нам википедия по поводу назначения этого шаблона:

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

Из определения можем сказать, что с помощью команд можно сделать приложение менее связанным друг с другом. Это может быть полезно при написании плагинов, когда есть различные реализации какого-то одного действия, когда надо запускать программу с произвольными начальными действиями определенными в командной строке к примеру.

Большая часть, если не всё, взаимодествие с польщователем в Visual Studio построено на командах. В качестве доказательства можете открыть Tools > Options > Environment > Keyboard и вашему взору предстанут команды. На каждую команду можно назначить горячую клавишу. Это же действие можно совершить из меню. Какие-то действия совершаются из плагинов к студии. Что-то делает студия сама, как результат запоминания ваших действий (открытие последних рабочих файлов при открытии проекта) – все это результат работы шаблона. Иначе была бы просто чудовищная связанность и Overobject – Oriented  Programming.

В минусы к этому методу можно записать неочевидность использования в итоговом коде и сложность исследования кода без исполнения его.

Общий вид шаблона представлен на диаграмме ниже.

Не пугаемся, в реальной жизни все как-то попроще будет.

Начало

Начнем с самого простого и элементарного способа.

Команда будет без параметров, просто будет содержимым для какого-либо действия.

public class Command {
    private readonly Action action;

    public Command(Action action) {
        this.action = action;
     }

    public void Execute() {
        if (action == null) return;
        action();
    }
}

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

В виду того, что я преподчитаю интерфейсный полиморфизм, для широты использования я выделил интерфейс из этого класса, куда поместил только основную команду на выполнение.

public interface ICommand {
    void Execute();
}

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

public static class CommandManager {
    private static readonly Dictionary<string, ICommand> commands = new Dictionary<string, ICommand>();

    public static void RegisterCommand(string commandName, ICommand command) {
        commands[commandName] = command;
    }

    public static void Execute(string commandName) {
        commands[commandName].Execute();
    }
}

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

Итак, в менеджере команд для хранения используется словарь, ключем будет имя команды. Сами команды записываются по интерфейсу. Для пущего удобства я советую определить статический класс с именами команд. Или же можно использовать для этого энумератор.

Использование же будет в духе

var command = new Command(DoJob);
CommandManager.RegisterCommand("Job", command);

И в нужном месте просто вызвать:

CommandManager.Execute("Job");

Улучшение уно

Второй способ расширяет возможности по узнованию и использованию команд. Для этого понадобится только немного улучшить Command Manager. Теперь уникальным ключем будет выступать не только имя команды но и ассоциированный с ней тип. Это позволит создавать одноименные команды для разных типов данных. Достигаться это будет с помощью типа кортеж.

public static class CommandManager {
    private static readonly Dictionary<Tuple<string ,Type>, ICommand> commands = new Dictionary<Tuple<string ,Type>, ICommand>();

    public static void RegisterCommand(string commandName, ICommand command, Type type) {
        commands[new Tuple<string, Type>(commandName,type)] = command;
    }

    public static void Execute(string commandName, Type type) {
        commands[new Tuple<string, Type>(commandName,type)].Execute();
    }
}

В остальном все то же самое.

Улучшение дуо

В качестве дальнейшей эволюции команд, было бы неплохо еще задавать условие исполнения команды. Для этого необходимо расширить класс Command. Условие будем задавать как функцию. Т.е. в класс Command дописываем свойство

public Func<bool> CanExecute { get; set;  }

Теперь можно изменить метод Execute и внести туда использование нового свойства.

public void Execute() {
    if(CanExecute() && action != null)
    action();
}

Улучшение трэо

Логическим развитием всего этого будет возможность передачи параметров в команду.

public class Command<T> : ICommand {
    public Action<T> Action { private get; set; }
    public Func<bool> CanExecute { private get; set; }

    private T parameter;

    public void SetParameter(object parameter) {
        this.parameter = (T) parameter;
    }

    public Command() {
        CanExecute = () => { return true; };
    }

    public void Execute() {
        if (CanExecute() && Action != null)
            Action(parameter);
    }
}

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

CommandManager меняется соответсвенно. Теперь перед в вызов команды будет опционально передаваться параметр.

public static void Execute(string commandName, Type type,object parameters = null) {
    var command = commands[new Tuple<string, Type>(commandName, type)];
    command.SetParameter(parameters);
    command.Execute();
}

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

И как тестовый пример (пусть и вырожденный в данном случае)

internal class MyClass {
    public MyClass() {
        var command = new Command<string> { Action = SomeAction };

        CommandManager.RegisterCommand(CommandNames.Open, command, GetType());

        CommandManager.Execute(CommandNames.Open, GetType());
        CommandManager.Execute(CommandNames.Open, GetType(), @"c:\temp\default.txt");
    }

    public void SomeAction(string s) {
        Console.Out.WriteLine("MC: " + s);
    }
}

Если это применять в другом классе, то будет в духе

CommandManager.Execute(CommandNames.Open, typeof(MyClass), "from main thread");

Такой вот менеджер команд должен был бы быть по идее в прошлой статье про межпроцессное взаимодействие.

Исходный код с примерами

Hard’n’heavy.

Комментарии (1) Пинги (0)
  1. hi!This was a really terrific theme!
    I come from roma, I was fortunate to come cross your theme in wordpress
    Also I get much in your Topics really thank your very much i will come again


Оставить комментарий


Нет обратных ссылок на эту запись.