Мой вопрос связан с шаблоном команды, где у нас есть следующая абстракция (код C#):
public interface ICommand
{
void Execute();
}
Возьмем простую конкретную команду, цель которой удалить объект из нашего приложения. Например, Person.
У меня будет DeletePersonCommand, который реализует ICommand. Эта команда требует, чтобы Person был удален в качестве параметра, чтобы удалить его при вызове метода Execute.
Как лучше всего управлять параметризованными командами? Как передать параметры командам перед их выполнением?
@Steven, спасибо за ссылку на ваш пост в блоге. Возможно, было бы хорошо, если бы вы могли пояснить, как подход, который вы описываете в нем, соответствует поставленному здесь вопросу, учитывая, что, по вашему собственному признанию, вы «не рассматриваете [это] как образец команды». Можно подумать, что ваш комментарий - это просто самореклама.





В конструкторе и хранится в виде полей.
Вы также захотите в конечном итоге сделать ваши ICommands сериализуемыми для стека отмены или сохранения файла.
DeletePersonCommand может иметь параметр в своем конструкторе или методах. DeletePersonCommand будет иметь Execute (), и в процессе выполнения можно проверить атрибут, который будет передан Getter / Setter перед вызовом Execute ().
Я бы добавил все необходимые аргументы в конструктор DeletePersonCommand. Затем, когда вызывается Execute(), используются параметры, переданные объекту во время создания.
Пусть «Человек» реализует какой-то интерфейс IDeletable, а затем заставит команду использовать любой базовый класс или интерфейс, который используют ваши сущности. Таким образом, вы можете создать команду DeleteCommand, которая пытается преобразовать сущность в IDeletable, и если это сработает, вызовите .Delete
public class DeleteCommand : ICommand
{
public void Execute(Entity entity)
{
IDeletable del = entity as IDeletable;
if (del != null) del.Delete();
}
}
Я не думаю, что это работает - весь смысл ICommand в том, что каждый подкласс переопределяет Execute () точно. Это решение требует, чтобы вызывающий Execute () знал более подробную информацию о типе вызываемой команды.
Я согласен с Мэттом. В любом случае этот класс DeleteCommand даже не компилируется, поскольку он не реализует void Execute (), как того требует интерфейс ICommand.
Используя внедрение зависимостей, вам все равно нужно знать все о типе команды, потому что вам нужно ее обновить! по крайней мере, так ваш код может быть универсальным, если вы имеете дело только с «Сущностью». Исходный ответ включает информацию об изменении ICommand для включения базового класса / интерфейса.
Вам нужно будет связать параметры с командным объектом либо с помощью конструктора, либо с помощью внедрения установщика (или эквивалентного). Возможно что-то вроде этого:
public class DeletePersonCommand: ICommand
{
private Person personToDelete;
public DeletePersonCommand(Person personToDelete)
{
this.personToDelete = personToDelete;
}
public void Execute()
{
doSomethingWith(personToDelete);
}
}
Именно то, что я бы сделал. Для всего, что неизвестно при создании команды, я бы передал интерфейс службе, которая получает объект при выполнении команды. Это может быть делегат, лямбда-выражение или другой объект.
Это плохое решение, потому что контейнер тесно связан с Person, вместо этого эта связь должна быть нарушена с помощью какого-то объекта Parameter, который содержит зависимости. «Скажи, не спрашивай» - вот главное правило.
@Blair Conrad Интересно, а что, если мы изменим метод приемника. В соответствии с принципом открытия / закрытия, изменение метода выполнения команды будет в порядке?
Есть несколько вариантов:
Вы можете передавать параметры по свойствам или конструктору.
Другой вариант:
interface ICommand<T>
{
void Execute(T args);
}
И инкапсулируйте все параметры команды в объект значения.
Проблема с приведенным выше кодом заключается в том, что разные команды (например, CreateSomeThingCommand и DeleteSomethingCommand) могут требовать разных параметров и больше не могут выполняться одинаково (учитывая вызов IEnumerable <ICommand> .Execute ()). Шаблон команды предназначен для использования для отделения определения от выполнения ... если вы передаете параметры во время выполнения, вы изменяете / контролируете поведение команды во время выполнения, а не во время определения.
Кстати: я думаю, вы имели в виду void Execute (T args) вместо Execute <T> (T args>, потому что T уже определен в ICommand <T>, второй на уровне функции / метода бесполезен. Вы можете создать тоже что-то вроде следующего: interface ICommand <T1> {void Execute <T2> (T1 t1, T2 t2);} (что делает больше смысла) или interface ICommand <T1> {void Execute <T2> (T2 t2); // использование T1 в другом месте}
Передайте человека при создании объекта команды:
ICommand command = new DeletePersonCommand(person);
так что когда вы выполняете команду, он уже знает все, что ему нужно знать.
class DeletePersonCommand : ICommand
{
private Person person;
public DeletePersonCommand(Person person)
{
this.person = person;
}
public void Execute()
{
RealDelete(person);
}
}
В этом случае то, что мы сделали с нашими объектами Command, - это создать объект Context, который по сути является картой. Карта содержит пары «имя-значение», где ключи являются константами, а значения - параметрами, которые используются реализациями Command. Это особенно полезно, если у вас есть цепочка команд, в которой последующие команды зависят от изменений контекста более ранних команд.
Таким образом, фактический метод становится
void execute(Context ctx);
Я использовал это в своем дизайне, но «Контекст» был Dictionary <string, object>.
На основе шаблона в C# / WPF интерфейс ICommand (System.Windows.Input.ICommand) определен для приема объекта в качестве параметра в Execute, а также метода CanExecute.
interface ICommand
{
bool CanExecute(object parameter);
void Execute(object parameter);
}
Это позволяет вам определить свою команду как статическое общедоступное поле, которое является экземпляром вашего настраиваемого объекта команды, реализующего ICommand.
public static ICommand DeleteCommand = new DeleteCommandInstance();
Таким образом, соответствующий объект, в вашем случае человек, передается при вызове execute. Затем метод Execute может привести объект и вызвать метод Delete ().
public void Execute(object parameter)
{
person target = (person)parameter;
target.Delete();
}
То, как «шаблон» реализован таким образом, представляет собой не что иное, как «специальный» делегат с проверкой (CanExecute). Я думаю, это теряет реальную функциональность, для которой создан шаблон ... разделения определения и выполнения команды. Передача параметров изменит / может изменить способ выполнения. Таким образом, определение команды берется из конструктора команды во время создания параметра. (Я знаю, что M $ использовал это для целей графического интерфейса, но я не думаю, что это должен быть общий подход для реализации шаблона команд.)
Вы должны создать объект CommandArgs, содержащий параметры, которые вы хотите использовать. Вставьте объект CommandArgs с помощью конструктора объекта Command.
Почему бы не ввести необходимые параметры или значение делегатом Func <MyParam>?
Передача данных через конструктор или сеттер работает, но требует, чтобы создатель команды знал, какие данные нужны команде ...
Идея «контекста» действительно хороша, и я работал над (внутренней) структурой, которая использовала ее некоторое время назад.
Если вы настроили свой контроллер (компоненты пользовательского интерфейса, которые взаимодействуют с пользователем, интерфейс командной строки, интерпретирующий пользовательские команды, сервлет, интерпретирующий входящие параметры и данные сеанса и т. д.), Чтобы предоставить именованный доступ к доступным данным, команды могут напрямую запрашивать данные, которые им нужны.
Мне очень нравится разделение, которое позволяет такая установка. Подумайте о наслоении следующим образом:
User Interface (GUI controls, CLI, etc)
|
[syncs with/gets data]
V
Controller / Presentation Model
| ^
[executes] |
V |
Commands --------> [gets data by name]
|
[updates]
V
Domain Model
Если вы сделаете это «правильно», те же самые команды и модель представления можно будет использовать с любым типом пользовательского интерфейса.
Если пойти дальше, то «контроллер» в приведенном выше примере довольно общий. Элементы управления пользовательского интерфейса должны знать только имя команды, которую они будут вызывать - им (или контроллеру) не нужно знать, как создать эту команду или какие данные ей нужны. В этом настоящее преимущество.
Например, вы можете сохранить имя команды для выполнения на карте. Каждый раз, когда компонент «запускается» (обычно это actionPerformed), контроллер ищет имя команды, создает ее экземпляр, вызывает execute и помещает ее в стек отмены (если вы его используете).
Моя реализация была бы такой (с использованием ICommand, предложенного Хуанмой):
public class DeletePersonCommand: ICommand<Person>
{
public DeletePersonCommand(IPersonService personService)
{
this.personService = personService;
}
public void Execute(Person person)
{
this.personService.DeletePerson(person);
}
}
IPersonService может быть IPersonRepository, это зависит от того, на каком «уровне» находится ваша команда.
Кажется, это лучший вариант использования шаблона стратегии, а не шаблона команды в вашем примере.
Уже упомянутый код от Блэра Конрада (не знаю, как его пометить) отлично работает если вы знаете, какого человека вы хотите удалить, когда создаете экземпляр класса, и его метода будет достаточно, но если вы не знаете, кого хотите удалить, пока не нажмете кнопку, вы можете создать экземпляр команды с помощью ссылка на метод, который возвращает человека.
class DeletePersonCommand implements ICommand
{
private Supplier<Person> personSupplier;
public DeletePersonCommand(Supplier<Person> personSupplier)
{
this.personSupplier = personSupplier;
}
public void Execute()
{
personSupplier.get().delete();
}
}
Таким образом, когда команда выполняется, поставщик выбирает человека, которого вы хотите удалить, делая это в момент выполнения. До этого времени у команды не было информации о том, кого удалять.
Полезный связь по поставщику.
ПРИМЕЧАНИЕ: код написан на java. Кто-то со знанием C# может это настроить.
Я знаю, что этот вопрос возник более четырех лет назад, но Хуанма и bloparod на самом деле дают правильный ответ: сделайте
ICommandобщим (ICommand<TArgs>). ДанныйTArgsинкапсулирует все аргументы (он становится Объект параметра). Вам нужно будет создать два объекта для каждой команды: один для сообщения; один для поведения. Поначалу это звучит неловко, но когда вы это поймете, вы никогда не оглянетесь назад. Эта статья подробно описывает эту модель. Обязательно к прочтению всем, кто читает этот вопрос.