Pull to refresh

Comments 23

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

command, этот антипаттерн, потому что скрывает за командами реальные классы и модули

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

это усложняет поиск класса которы привязан к команде

Каким образом? Все зависимости читаются в конструкторе класса (команды в нашем случае). Либо прямо по месту использования можно по одному клику провалиться в реализацию интересующего нас метода.

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

Например, классический пример -- это реализация undo\redo функционала в приложении. Приложение фиксирует последовательность команд, запущенных пользователем и если все команды закрыты одним интерфейсом

Stack<ICommand> _commandsHistory;

interface ICommand
{
  void Undo();
}

то, при необходимости откатить изменения, мы можем просто вытягивать элементы стэка _commandsHistory и вызывать ICommand.Undo(), абстрагировавшись от реализации этих самых команд.

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

Вообще то нет, вы описываете обычный Virtual Function, а комманда в своём первоначальном описании:

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

При чем тут Virtual Function, что из того что я описал хоть как-то может относиться к виртуальным функциям?

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

Вы не согласны именно с моим утверждением про то что это паттерн "для объединения множества операций под единым интерфейсом" или с тем что у реализации этого паттерна не обязательно должны быть внешние зависимости?

не согласен с

для объединения множества операций под единым интерфейсом

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

Единый интерфейс даёт виртуальный метод

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

Например, у нас в коде определены несколько сущностей с похожим функционалом -- Cat, Dog, Human, реализующие метод Run(). Если мы хотим в коде смоделировать стадион, по которому будут бегать экземпляры наших людей, кошек и собак (т.е. нам не важно как именно бегает конкретный инстанс объекта, на четырех ногах или на двух, нам важно только знать, что этот объект умеет бегать), то что мы будем использовать? Виртуальные методы (честное слово, не представляю как) или все-таки для этого используются интерфейсы?

я понял, о чем вы говорите. Постараюсь объяснить.

Абстракцию "интерфейс" можно рассматривать как частный случай абстракции "виртуальный метод". Во многих объектно-ориентированных языках абстрактные методы являются частным случаем виртуальных методов, при этом абстрактный метод — это виртуальный метод без реализации, который требует обязательного переопределения в классах-потомках. Интерфейсы же по сути содержат только абстрактные методы (без реализации), задавая контракт, который должны реализовывать классы.

Не все языки имеют конструкции языка - интерфейсы.

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

virtual void methodName() = 0;

В случае с Командой, это не инкапсуляция, а завуалирование. Например в dry-rb команда в ruby, реализована так, что классы превращаются по сути в методы, из "Камел Кейс" в "Снейк Кейс", и ни что не ограничивает, автора кода, назвать этот метод вообще как угодно, и потом вы фиг что найдете в этом коде даже через ручной поиск, а IDE и подавно не поймет откуда уши растут.

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

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

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

автор не адаптировал паттерн Command, а заменил команду на сервис

Проблема в том, что нет такого паттерна как Service.

Когда мы говорим о Service имеется ввиду некое общее определение:

Service — обычно структурный или фасадный паттерн, инкапсулирующий определённую бизнес-логику или предоставляющий набор операций (сервис), которые используют другие компоненты приложения.

И да, после модификации Command вышел за границы своей изначальной ответсвенности, и по сути это уже не Command.

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


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

PS Возможно предлагаемый подход больше напоминает Function Object Pattern, который не столь широко известен.

Проблема в том, что нет такого паттерна как Service.
Возможно стоит именовать предложенный подход как то иначе?

Ну как это нет. Он называется "Бизнес-логика в сервисах". Только это не паттерн, а архитектурный подход.

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

Можно было обойтись "визитером"

"Визитер" ? Вникнуть в код с ним сложнее чем в код Command.

Visitor позволяет вынести логику обработки структуры объектов вовне: новые операции добавляются через отдельные классы–посетители, а не внутри изменяемых объектов.

Мне кажется, в вашем подходе теряется главное - абстрактность команд.

Execute идет без параметров не потому, что так модно, а потому, что все команды поддерживают один интерфейс, разнородные команды можно складывать в очередь, выполнять позже, сериализовать и десериализовать, передавать по сети и т.д.
Ну и потребителю команды должно быть плевать, о чем она.
Команды - это функции с ООП оберткой.
Если же она принимает явные параметры - теряется суть паттерна. Это становится просто анемичной функцией с архитектурной ООП космонавтикой.
Ваша статья примерно так и выглядит, уж извините. Фабрика для порождения команды, которой передается сервис и параметр, чтобы у сервиса вызвать параметр - выглядит как FooBar ООП едишн и непонятно, что решает.


А между тем сама проблема довольно сложная и важная - задача разделения данных для выполнения (фактически параметров команды) и зависимостей довольно важна, ровно для того, чтобы не таскать везде зависимости (что само по себе противоположно тому, зачем нужны команды).

Простой пример, который это реализует, может выглядеть так:
- Это пример для команд, которые сохраняются в очередь и передаются по сети, чтобы клиент и сервер одинаково их применяли.
- Команда делается из нескольких классов, собственно команды (класс определенного типа с данными) и ее исполнителя - специализированного класса, который содержит логику и набор зависимостей, которые нужны команде для работы.
- Исполнители создаются лениво и 1 раз для 1 типа комманды
- При старте команды сохраняется собственно класс данных (он же сериализуется и может быть отложен). При необходимости выполнения исполнитель команд находит/создает в списке специализированный исполнитель и отдает ему команду.

Это же можно сделать шиворот-навыворот - команда описывается с Execute, который требует некоторый тип Context класса с зависимостями.

Если кому то интересно, могу накидать код, все ж с рабочего проекта копипастить не очень (да и кривенький)

В подходе ничего не теряется, Command как был так и остался, никто его не запрещал, в моём подходе он был изменён для решения других задач.

Это статья не про улучшение Command.

В примечании в конце статьи указал, что видимо надо было танцевать от Function Object, было бы меньше недопонимания.

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

Обработать REST-запрос, обработать background-job, считав параметры из БД, обработать _любую_ команду UI - это команды. Команду можно единообразно повторить, отменить, залогировать. А ICreateUser может только пользователя создать. Здесь нет унификации с определением общего функционала, которые являются определяющим мотивом выбора паттерна Команда.

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

Я за звание специалиста не лезу, но
1)

var createUserCommand = new CreateUser(userProvider, userName);

Вроде все про DI, но почему команда создания юзера явно вызывается? Скорее всего там через CreateUserCommandFactory вызов должен идти

2) Я так и не увидил, так в чем собственно преимущество использования комманд, если биндинги
явные CreateUser

и что мешает сделать биндинг .addService<ICreateUser, CreateUser>() ибо описанное в статье ничем не отличается явным указанием сервиса в зависимястях ибо ничем от обычного сервиса он не отличается...

Сравните и найдите 10 отличий:

interface ICreateUser
{
   UserDto Execute(string userName);
}
interface IUserService
{
   UserDto CreateUser(string userName);
}

Первое — это Команда, не перепутайте! :)

Только, чтоб ICreateUser можно было "создать по месту", нам ещё нужен ICreateUserCommandFactory, или IServiceProvider. Это расстраивает.

В принципе, не существует закона, который бы нам запрещал обычный интерфейс сервиса, распиленный на десятки под каждый его метод назвать "командой". Или назвать "командой" что-нибудь ещё, например банальные строки, и пусть в строках будет код на C#, или на JS. Тоже команда :)

Единственное, чего я так и не понял: какую задачу решали, и зачем это всё.

Первое — это Команда, не перепутайте! :)

Тут нечего путать. Сервис содержит НАБОР методов управляющих сущностью User, в вашем примере сервис имеет только один метод, что не отменяет его границы ответственности.
Команда - отдельный метод, функция реализованная в виде объекта.

Для меня разница между этими двумя архитектурными сущностями очевидна, что я и попытался донести в 2-х статьях.

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

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

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

Я перечитал вашу статью, и всё же не увидел, какую конкретно проблему или задачу вы пытаетесь решить. Уж простите, наверное плохо читаю. Поэтому и попросил хотя бы в трёх словах пояснить.

Спасибо.

Sign up to leave a comment.

Articles