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

1. Иерархия классов, над которыми будет твориться колдовство

Предположим, что у нас есть абстрактный класс «транспортное средство» (Vehicle), которое может двигаться (метод Move). У этого класса есть три потомка: автомобиль, самолёт и рикша, каждый из которых по-своему реализует этот метод.
abstract class Vehicle
{
	abstract void Move();
}

class Car : Vehicle
{
	override void Move()
	{
		// burn fuel
		// spin wheel
	}
}

class Plane : Vehicle
{
	override void Move()
	{
		// suck air
		// burn fuel
		// spew jetstream
	}
}

class Rickshaw : Vehicle
{
	override void Move()
	{
		// do one step
		// beg white master for money
	}
}


2. Применение стратегии

Предположим, что у нас начинают появляться новые требования:
  1. Стало очевидно, что скоро появятся новые типы транспортных средств.
  2. Некоторые из них будут реализовывать метод Move одинаковым образом. Например, и машина, и тепловоз будут сжигать топливо и крутить колёса.
  3. Способ движения может быть изменён. Например, пароходофрегат может плыть и под парусами, и на паровой тяге.
Очевидно, настало время выделить код, ответственный за движение, в отдельный класс Двигатель (Engine).
abstract class Vehicle
{
	Engine Engine
	{
		get { return engine; }
		set
		{
			if (value != null)
			{
				engine = value;
			}
			else
			{
				throw new ArgumentNullException();
			}
		}
	}
	private Engine engine;

	protected Vehicle(Engine engine)
	{
		Engine = engine;
	}

	public void Move()
	{
		engine.Work();
	}
}

class Car : Vehicle
{
	Car()
		: base(new InternalCombustionEngine())
	{ }
}

class Plane : Vehicle
{
	Plane()
		: base(new JetEngine())
	{ }
}

class Rickshaw : Vehicle
{
	Rickshaw()
		: base(new Slave())
	{ }
}

abstract class Engine
{
	public abstract void Work();
}

class InternalCombustionEngine : Engine
{
	public override void Work()
	{
		// burn fuel
		// spin wheel
	}
}

class JetEngine : Engine
{
	public override void Work()
	{
		// suck air
		// burn fuel
		// spew jetstream
	}
}

class Slave : Engine
{
	public override void Work()
	{
		// do one step
		// beg white master for money
	}
}


3. Сокращаем код, если это возможно

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

abstract class Vehicle<EngineT> : Vehicle
	where EngineT: Engine
{
	protected Vehicle()
		: base(new EngineT())
	{ }
}

class Car : Vehicle<InternalCombustionEngine>
{
	Car()
		: base(new InternalCombustionEngine())
	{ }
}

class Plane : Vehicle<JetEngine>
{
	Plane()
		: base(new JetEngine())
	{ }
}

class Rickshaw : Vehicle<Slave>
{
	Rickshaw()
		: base(new Slave())
	{ }
}

...
Если классы двигателей имеют конструкторы без параметров, то этим тоже стоит воспользоваться, добавив constraint new() к типопараметру EngineT.
...

abstract class Vehicle<EngineT> : Vehicle
	where EngineT: Engine, new()
{
	protected Vehicle()
		: base(new EngineT())
	{ }
}

class Car : Vehicle<InternalCombustionEngine>
{ }

class Plane : Vehicle<JetEngine>
{ }

class Rickshaw : Vehicle<Slave>
{ }

...


4. Добавляем ещё разнообразия

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

abstract class Vehicle<EngineT> : Vehicle
	where EngineT : Engine
{
	protected Vehicle()
		: base(Engine.GetFromWarehouse<EngineT>())
	{ }
}

...

abstract class Engine
{
	abstract void Work();

	private static readonly IDictionary<Type, Engine> warehouse = new Dictionary<Type, Engine>
	{
		{ typeof(InternalCombustionEngine), new InternalCombustionEngine() },
		{ typeof(JetEngine), new JetEngine() },
		{ typeof(Slave), new Slave() },
	};

	static Engine GetFromWarehouse<EngineT>()
		where EngineT : Engine
	{
		return warehouse[typeof(EngineT)];
	}
}

...
Как точно подметил soalexmn, за что ему спасибо, мы только что наблюдали паттерн Service locator. Как видите, чем дальше в лес, тем меньше остаётся от стратегии.

5. А можно расшарить один двигатель на несколько машин?

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


Буду рад, если эти заметки окажутся кому-либо полезными.
Поделиться с друзьями
-->

Комментарии (43)


  1. INC_R
    28.02.2017 11:40
    +3

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

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


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

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


    1. courage_andrey
      28.02.2017 13:01

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


      1. INC_R
        28.02.2017 13:27
        +1

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


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


        1. courage_andrey
          28.02.2017 13:54
          -1

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


          1. INC_R
            28.02.2017 14:13

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

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


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

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


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

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

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


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


            1. courage_andrey
              28.02.2017 15:31

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

              car.RemoveEngine();

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

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


              1. INC_R
                28.02.2017 17:47

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

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


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


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


                1. areht
                  28.02.2017 23:37

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

                  Если принять, что этот «двигатель» — типа стратегия, и внутреннего состояния не имеет, то, в общем то, без разницы сколько там инстансов.


                1. courage_andrey
                  01.03.2017 07:55

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


  1. soalexmn
    28.02.2017 13:01

    Возможно я чего-то не понимаю, но в итоге получается какая-то странная реализация стратегии! Обычно у нас есть несколько реализаций стратегии, и мы передаем нужную стратегию в объект.
    А в вашем примере Engine.GetFromWarehouse() это service locator почти в чистом виде.
    Лучше уж сразу использовать DI в таком случае и уже на стороне контейнера настраивать логику инъекции двигателя.


    1. courage_andrey
      28.02.2017 13:09

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


      1. soalexmn
        28.02.2017 13:43

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


  1. oxidmod
    28.02.2017 13:10

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


    1. courage_andrey
      28.02.2017 13:34

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


      1. INC_R
        28.02.2017 14:29

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


        1. courage_andrey
          28.02.2017 14:58

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

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


          1. INC_R
            28.02.2017 15:30
            +1

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


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


            1. courage_andrey
              28.02.2017 15:36

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


              1. dymanoid
                28.02.2017 15:59

                Рекомендую заглянуть хотя бы на википедию, а лучше почитать мэтров.

                Основная задача стратегии — иметь возможность динамического выбора алгоритма, а вовсе не инкапсуляция.

                the strategy pattern is a behavioural software design pattern that enables an algorithm's behavior to be selected at runtime.
                (выделение моё).


              1. INC_R
                28.02.2017 16:04

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


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

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

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


                1. courage_andrey
                  28.02.2017 16:34

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

                  Обратите внимание, что классический паттерн Стратегии весьма абстрактен.

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

                  Если есть требование менять мотор прямо в полёте — не проблема, просто код станет чуточку длиннее.


                  1. INC_R
                    28.02.2017 16:48

                    И в третий раз повторяю: можно «не прибивать»

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


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

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


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

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


                    1. courage_andrey
                      28.02.2017 17:21

                      Цитирую Ваше сообщение:

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


                      1. INC_R
                        28.02.2017 17:27

                        Ничто не мешает сделать двигатель параметром конструктора.

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


                        1. courage_andrey
                          01.03.2017 08:15

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


                          1. INC_R
                            01.03.2017 11:00

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


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


                            1. courage_andrey
                              01.03.2017 11:21

                              Понимаете, даже первый пример не является стратегией.… Поэтому получается, что статья о том, как из стратегии сделать не-стратегию.
                              Согласен на 100%. Постараюсь сегодня после работы дописать недостающее, чтобы сложить эту мозаику целиком, без «ну вы поняли, что я подразумевал».
                              Насчёт сделать generic-ом тип клиента надо подумать. Я попробую поиграться с этим use case в отвязке от транспорта, потому что с ним постановка выглядит действительно дико.


                            1. courage_andrey
                              01.03.2017 23:06
                              +1

                              Допереписал. Ещё раз спасибо, что потратили своё терпение, чтобы донести до меня эту идею.


                              1. a-tk
                                02.03.2017 08:55

                                И эта реализация имеет проблемы: Раб он как бы не совсем двигатель. В данном случае корректнее будет ввести интерфейс IEngine вместо абстрактного базового класса Engine.


                                1. courage_andrey
                                  02.03.2017 09:24

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


                                1. INC_R
                                  02.03.2017 11:24

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


                  1. areht
                    28.02.2017 22:37

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

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

                    Можно обсуждать коротко ли это, или хорошо ли это, но это не стратегия.


                    1. courage_andrey
                      01.03.2017 08:35

                      Цитирую свой ответ на один из предыдущих комментариев:

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


  1. a-tk
    01.03.2017 07:32

    Зависимость базового класса от потомков в последнем примере — в общем-то не слишком хорошо.
    Однако этот код может стать основой для загрузки типов динамически с помощью рефлексии по модели плагинов. Но это уже совсем другая история.


    1. courage_andrey
      01.03.2017 07:44

      Согласен, привязывать базовый класс к потомкам — не лучшая идея. Пример, как можно заметить, сильно упрощён (и судя по комментариям, я зря опустил ряд шагов, посчитав их слишком очевидными). Я бы решал задачу заполнения словаря 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;
      	}
      }
      


      1. a-tk
        01.03.2017 08:13

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


        1. courage_andrey
          01.03.2017 08:34

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


          1. a-tk
            01.03.2017 09:04

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


            1. courage_andrey
              01.03.2017 09:49

              Сделать всё на основе прямых вызовов, когда проект не монолитный и допускает расширения, всё равно не получится.

              WinForms-компоненты от DevExpress так работали лет 10 назад, если мне память не изменяет.


              1. a-tk
                01.03.2017 11:21

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


                1. courage_andrey
                  01.03.2017 11:23

                  Да, именно об этом я и говорю.


  1. FessEmpty
    01.03.2017 09:43

    Это не паттерн стратегия, это паттерн приспособленец(Flyweight)


    1. courage_andrey
      01.03.2017 09:45

      Исходя из логики, которая руководила действиями, — нет. Только выглядит похоже.