Pull to refresh

Comments 43

Предположим теперь, что двигателей в этом примере есть всего три (да, именно три экземпляра), а транспортных средств – много, но они все могут по несколько раз использовать один и тот же двигатель, разделяя его между собой. (Это хороший пример с точки зрения кода, но отвратительный точки зрения логики реального мира: переставлять двигатель с одной машины на другую — весьма сомнительное удовольствие.)

Эээээ. С точки зрения кода этот пример не особо лучше "реального мира". Это не "переставлять" двигатель. Это значит: к одному двигателю прицепить все машины в мире.


var car1 = new Car();
var car2 = new Car();
car1.Move();

Все дружно поедут?

Это не «переставлять» двигатель. Это значит: к одному двигателю прицепить все машины в мире.
Абсолютно верно.
Все дружно поедут?
А вот это уже зависит от реализации. В реальном мире один мотор засунуть в несколько машин не получится, потому что машины не сделаны потокобезопасными. :) А в мире программирования не только получится, но даже иногда и приходится. Да хранят нас средства синхронизации!

А причем здесь вообще потоки? Здесь должна быть не потокобезопасность, а гарантия, что всегда существует не более одного инстанса любого типа, использующего данный двигатель. А раз речь идет о C#, где объекты чистятся GC, а не руками, то таких гарантий вообще нельзя дать.
И да. У вас в двигателе написано: // burn fuel // spin wheel. Видимо, топливо из общего глобального бензобака, и крутим глобальные колеса, которые прилеплены ко всем автомобилям мира?


Зачем такая сложность? В чем проблема создавать двигатель заново для каждого конкретного автомобиля?

Здесь должна быть… гарантия, что всегда существует не более одного инстанса любого типа, использующего данный двигатель.
Вы говорите верно с точки зрения логики реального мира, в котором один двигатель действительно нельзя засунуть в две кредитопомойки.
Видимо, топливо из общего глобального бензобака, и крутим глобальные колеса, которые прилеплены ко всем автомобилям мира?
Да, всё понято абсолютно верно. Другими словами: я родил не слишком удачный пример. Чтобы звучало чуть более логично, я попробую переформулировать задачу следующим образом: «Предположим, на складе есть один рабочий двигатель, который подходит к нескольким транспортным средствам. Таких в наличии имеется три: трактор для вспахивания земли, Волга председателя для выезда в райцентр и (внезапно) дизель-генератор для освещения сельского клуба. Трактор работает только днём по будням, председатель катается днём в выходные, а танцы в клубе происходят каждую ночь.» В таком случае метод GetFromWarehouse заменяется слесарем Петровичем в промасленной робе, который перекидывает движок из одного места в другое, а синхронизация потоков (случаи выездов в поле и в райцентр во время танцев) не нужна в связи с поставленной задачей. Но если председателю припрёт скататься в райцентр посреди уборочной (описанная Вами ситуация), то да, всё будет плохо и Петровичу придётся уходить в запой.
В чем проблема создавать двигатель заново для каждого конкретного автомобиля?
Это может быть накладно по ресурсам. Такой «двигатель» может быть не просто Console.WriteLine(«Hello, World»), а тяжеловесным ресурсом ОС, работающим через COM и маршалинг. Абстрагируйтесь от машин, они здесь мешают.
Вы говорите верно с точки зрения логики реального мира, в котором один двигатель действительно нельзя засунуть в две кредитопомойки.

Почему нельзя? Можно. Только не так, как в примере, а, упрощенно, так:


var car = new Car();
car.SetEngine(engine);
...
car.RemoveEngine();

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


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

Это может быть накладно по ресурсам.

Почему бы не сделать пул двигателей?


var car = new Car();
car.SetEngine(enginePool.Acquire());
...
var engine = car.RemoveEngine();
enginePool.Add(engine);
var car = new Car();
car.SetEngine(engine);

car.RemoveEngine();

Абстрагируйтесь, пожалуйста, от автомобилей. Если говорить про код, а не про автомобили, то никто и ничто в этом мире не мешает нам использовать один и тот же экземпляр дважды.
IБуксир буксир1 = new Пароход();
баржаСЗерном.ПрицепитьК(буксир1);
баржаСУглем.ПрицепитьК(буксир1);
паромСТуристами.ПрицепитьК(буксир1);

Пул — это здорово, это дальнейшее развитие идеи статьи с целью увеличить число поддерживаемых use case-ов. Можно, конечно, и так.
Абстрагируйтесь, пожалуйста, от автомобилей. Если говорить про код, а не про автомобили, то никто и ничто в этом мире не мешает нам использовать один и тот же экземпляр дважды.

Да в общем-то даже один двигатель можно прицепить к двум машинам сразу, было бы желание и инструмент. Пример с буксиром в этом плане замечателен. Тут сразу видно, что мы один буксир прицепили к 3 плавсредствам. Если нам так надо было, не вижу проблем.


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


  • Создавая инстанс автомобиля, я не могу узнать о том, что внутри там один двигатель, это крайне неочевидный контракт.
  • Если я это уже знаю, то этого может не знать мой коллега, который в другой части системы создаст инстанс автомобиля и угробит все приложение.
  • Нет средств контроля того, у кого двигатель, его нельзя отнять у владельца
  • Перестав использовать один инстанс автомобиля, мы просто не можем создать еще один, потому что старый может быть все еще не собран GC и возможны всякие интересные эффекты.
> Проблема вашего решения с общим двигателем ровно в том, что это все происходит неявно и непредсказуемо.

Если принять, что этот «двигатель» — типа стратегия, и внутреннего состояния не имеет, то, в общем то, без разницы сколько там инстансов.
Всё, о чём Вы говорите — это (дополнительные) требования. Естественно, как только требований становится больше, решение начинает их учитывать и становится сложнее. Пример в статье простой, как грабли, и там ничего не сказано ни про количество двигателей, ни про количество владельцев, ни про потребляемые ими ресурсы. Когда понадобится усложнение, я думаю, каждый догадается, что и к чему надо прикрутить.
Возможно я чего-то не понимаю, но в итоге получается какая-то странная реализация стратегии! Обычно у нас есть несколько реализаций стратегии, и мы передаем нужную стратегию в объект.
А в вашем примере Engine.GetFromWarehouse() это service locator почти в чистом виде.
Лучше уж сразу использовать DI в таком случае и уже на стороне контейнера настраивать логику инъекции двигателя.
А в вашем примере Engine.GetFromWarehouse() это service locator почти в чистом виде.
Да, согласен. Я бы сказал, что последний абзац описывает процесс перехода от одного к другому.
Лучше уж
Вопросы «лучше бы ...» всегда попахивают началом священных войн.Я не люблю спорить на тему того, какой из двух подходов лучше, потому что in real life приходится, как правило, использовать оба. Всё зависит от конкретной ситуации: требования, среда выполнения, legacy, перспективы развития, командный пинок сверху и т.д.

Да, DI и service locator часто используют вместе, не буду спорить. В примере на стороне ioc-контейнера будет как раз стоять фабрика/стратегия.

UFO just landed and posted this here
С фабричным методом я не согласен, потому что что ни Vehicle не служит для порождения Engine, ни наоборот. Про прототип — я бы сказал, не совсем 100%. Если честно, то судя по комментариям (весьма справедливым, надо признать), название статьи нужно сменить на что-то вроде «Я прицепил к стратегии generic, и смотрите сколько ещё паттернов повылазило.»

Я бы поспорил насчет стратегии, кстати. Это паттерн подразумевает, что клиенту можно подсунуть любую стратегию. Здесь же клиент создает ее сам (или получает ее откуда-то, но тип заранее известен), и предметная область подразумевает, что к каждому транспорту применим только один тип двигателя (нельзя вкрутить в самолет рикшу, например). Тут нет стратегии, это обычное наследование.

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

Само по себе вынесение двигателя из транспортного средства — это не стратегия. Не каждое вынесенное куда-то поведение есть стратегия.
Стратегию характеризует возможность замены этого поведения во время работы. Т.е. надо либо просунуть стратегию (двигатель) через конструктор конкретного клиента (машины, самолета), либо запросить его из клиента без указания конкретного типа стратегии.


Здесь этого нет, потому что каждый клиент (машина, рикша, самолет) сам говорит, с каким именно типом двигателя он работает (через Vehicle, через new FooEngine(), через Engine.GetEngine(). По сути это все разные способы создать двигатель определенного типа.

Стратегию характеризует возможность замены этого поведения во время работы. Т.е. надо либо просунуть стратегию (двигатель) через конструктор конкретного клиента (машины, самолета), либо запросить его из клиента без указания конкретного типа стратегии.
По-моему, больше в сути стратегии всё-таки инкапсуляции выносимого поведения, чем в возможности горячей замены. Но одно другому не мешает. Говоря в терминах статьи, рикша вполне может пересесть на велосипед.
UFO just landed and posted this here

Как раз наоборот. Например, смотрим пост SergeyT тут: http://sergeyteplyakov.blogspot.ru/2014/02/singleton-pattern.html:


По определению, применение стратегии обусловлено двумя причинами: (1) инкапсуляция поведения или алгоритма и (2) возможность замены поведения или алгоритма во время исполнения. Любой нормально спроектированный класс уже инкапсулирует в себе поведение или алгоритм, но не любой класс с некоторым поведением является или должен быть стратегией. Стратегия нужна тогда, когда нужно не просто спрятать алгоритм, а когда нам важно иметь возможность заменить его во время исполнения!

Другими словами, стратегия обеспечивает точку расширения системы в определенной плоскости: класс-потребитель стратегии не знает, как выполняется некоторое действие и кто именно его выполняет; об этом знают классы более высокого уровня.

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

И в третий раз повторяю: можно «не прибивать». И подменять во время исполнения никто не мешает. Цитирую статью по ссылке:
Обратите внимание, что классический паттерн Стратегии весьма абстрактен.

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

Если есть требование менять мотор прямо в полёте — не проблема, просто код станет чуточку длиннее.
И в третий раз повторяю: можно «не прибивать»

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


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

Получает. А не создает сам. В ваших примерах именно самостоятельное создание контекстом (автомобилем, самолетом, рикшей) конкретного двигателя. Следовательно, это не стратегия.


Если есть требование менять мотор прямо в полёте

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

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

Да, безусловно. Тогда можно будет вести речь о том, что это стратегия. Но если это сделать, то все упрощения и generic-и из статьи уже неприменимы. Или надо делать Car, Plane и т.п. тоже generic и параметризовать типом двигателя.

Дошло, наконец, откуда непонимание. То, что получается ближе к финалу, уже не напоминает стратегию. Это мне тоже очевидно. Стоит ли сменить название статьи на «Простой способ сократить код после применения паттерна «стратегия» с помощью использования generic-классов»? И, возможно, вставить в самое начало пример того, как отрабатывает сам паттерн? Потому что комментарии однозначно на это намекают.

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


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

Понимаете, даже первый пример не является стратегией.… Поэтому получается, что статья о том, как из стратегии сделать не-стратегию.
Согласен на 100%. Постараюсь сегодня после работы дописать недостающее, чтобы сложить эту мозаику целиком, без «ну вы поняли, что я подразумевал».
Насчёт сделать generic-ом тип клиента надо подумать. Я попробую поиграться с этим use case в отвязке от транспорта, потому что с ним постановка выглядит действительно дико.
Допереписал. Ещё раз спасибо, что потратили своё терпение, чтобы донести до меня эту идею.
И эта реализация имеет проблемы: Раб он как бы не совсем двигатель. В данном случае корректнее будет ввести интерфейс IEngine вместо абстрактного базового класса Engine.
Объявление раба двигателем — историческая шутка, отсылающая к латинскому выражению «говорящее орудие», употреблённое в отношении рабов философом Варроном в его трудах, посвящённых сельскому хозяйству. Особенности использования интерфейсов вместо абстрактных классов и наоборот — это тема, которой хватит на самостоятельную статью, пару холиваров и три программистские пьянки.

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

> Если есть требование менять мотор прямо в полёте

Это не требование, это определение «стратегии». То, что вы сделали — не «стратегия».

Можно обсуждать коротко ли это, или хорошо ли это, но это не стратегия.
Цитирую свой ответ на один из предыдущих комментариев:
Дошло, наконец, откуда непонимание. То, что получается ближе к финалу, уже не напоминает стратегию. Это мне тоже очевидно. Стоит ли сменить название статьи на «Простой способ сократить код после применения паттерна «стратегия» с помощью использования generic-классов»? И, возможно, вставить в самое начало пример того, как отрабатывает сам паттерн? Потому что комментарии однозначно на это намекают.
Зависимость базового класса от потомков в последнем примере — в общем-то не слишком хорошо.
Однако этот код может стать основой для загрузки типов динамически с помощью рефлексии по модели плагинов. Но это уже совсем другая история.
Согласен, привязывать базовый класс к потомкам — не лучшая идея. Пример, как можно заметить, сильно упрощён (и судя по комментариям, я зря опустил ряд шагов, посчитав их слишком очевидными). Я бы решал задачу заполнения словаря warehouse не с помощью рефлексии, а предоставив классам, имеющим на то причины, возможность вызвать примерно такой метод:
abstract class Engine
{
	...

	private static readonly IDictionary<Type, Engine> warehouse = new Dictionary<Type, Engine>();

	public /*internal, protected - выбрать по необходимости*/ static void RegisterEngine<EngineT>(EngineT engine)
		where EngineT : Engine
	{
		warehouse[typeof(EngineT)] = engine;
	}
}
Такое решение имеет другую проблему: в какой момент и кто должен вызвать этот метод?
В некоторых скриптовых языках базовый класс может получить уведомление о том, что в области видимости появился его потомок. В .NET такое сделать невозможно в силу ленивости сборок и классов.
От требований зависит, кто будет вызывать, деталей реализации и контекста вызова. Из нормальных примеров, что я встречал, вызывающий код перед использованием двигателей сам говорит, какие их типы он хочет использовать. Из кривых способов я видел заполнение этого списка статическим конструктором типа двигателя, который искал своих неабстрактных потомков в загруженных сборках, и статическими конструкторами дочерних типов, которые регистрировали себя в классе-родителе. Но эти два способа — откровенный выстрел себе в ногу, они работали без проблем только благодаря упоротости и перфекционизму человека, занимавшегося сопровождением кода.
У меня в проектах достаточно часто встречается сборка всех потомков рефлексией. Но не в статическом конструкторе, а через статическое свойство, которое инициализирует список потомков лениво, при первом запросе.
Эта практика хорошо работает, если надо делать поддержку различных входных данных или выбор пользователем каких-то вещей.
Сделать всё на основе прямых вызовов, когда проект не монолитный и допускает расширения, всё равно не получится.
Сделать всё на основе прямых вызовов, когда проект не монолитный и допускает расширения, всё равно не получится.

WinForms-компоненты от DevExpress так работали лет 10 назад, если мне память не изменяет.
Тогда нужен динамический регистратор. В .NET в сборках банально нету кода, который запускается при загрузке её в домен приложения, то есть аналога DllMain.
Это не паттерн стратегия, это паттерн приспособленец(Flyweight)
Исходя из логики, которая руководила действиями, — нет. Только выглядит похоже.
Sign up to leave a comment.

Articles