Эта заметка содержит ряд хитростей, позволяющих сократить код, получившийся после применения паттерна «стратегия». Как нетрудно догадаться из названия, все они будут так или иначе связаны с использованием generic-типов.
Это вторая версия статьи. Первая (под названием Набор мелких улучшений работы с паттерном «стратегия» с помощью generic-классов) получилась не очень удачной, так как в ней в духе Ландау и Лифшица было опущено несколько промежуточных шагов, критически важных для понимания хода мысли. Объявляю отдельную благодарность INC_R, сумевшему-таки в комментариях донести до меня этот простой факт.
Буду рад, если эти заметки окажутся кому-либо полезными.
Это вторая версия статьи. Первая (под названием Набор мелких улучшений работы с паттерном «стратегия» с помощью 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. Применение стратегии
Предположим, что у нас начинают появляться новые требования:- Стало очевидно, что скоро появятся новые типы транспортных средств.
- Некоторые из них будут реализовывать метод Move одинаковым образом. Например, и машина, и тепловоз будут сжигать топливо и крутить колёса.
- Способ движения может быть изменён. Например, пароходофрегат может плыть и под парусами, и на паровой тяге.
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);
Буду рад, если эти заметки окажутся кому-либо полезными.