Pull to refresh

Простые шаги по сокращению кода после применения паттерна «стратегия» с помощью generic-классов

Reading time 4 min
Views 6.7K
Эта заметка содержит ряд хитростей, позволяющих сократить код, получившийся после применения паттерна «стратегия». Как нетрудно догадаться из названия, все они будут так или иначе связаны с использованием 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);


Буду рад, если эти заметки окажутся кому-либо полезными.
Tags:
Hubs:
+5
Comments 43
Comments Comments 43

Articles