📜 ⬆️ ⬇️

Simple steps to reduce the code after applying the strategy pattern using generic classes

This note contains a number of tricks to reduce the code resulting from the application of the strategy pattern. As you might guess from the name, all of them will be somehow connected with the use of generic types.
This is the second version of the article. The first one (called the Set of minor improvements to work with the “strategy” pattern using generic classes ) was not very successful, since in it, in the spirit of Landau and Lifshitz, several intermediate steps that are crucial for understanding the train of thought were omitted. I declare a special thanks to INC_R , who managed to convey to me this simple fact in the comments.

1. The hierarchy of classes over which witchcraft will be created

Suppose that we have an abstract vehicle class (Vehicle) that can move (the Move method). This class has three descendants: a car, a plane and a rickshaw, each of which implements this method in its own way.
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. Application of strategy

Suppose that new requirements are beginning to appear here:
  1. It became obvious that new types of vehicles would soon appear.
  2. Some of them will implement the Move method in the same way. For example, both the car and the diesel locomotive will burn fuel and turn the wheels.
  3. The way of movement can be changed. For example, a steamboat frigate can sail both under sail and on a steam train.
Obviously, it is time to separate the code responsible for the movement into a separate class Engine (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. Reduce the code if possible

If it is found on some fragment of the vehicle hierarchy that the engine is not necessary to change in the course of work, then the hands will stretch to parameterize the class of such a vehicle by the type of its engine in order to save a few extra lines of code. For the sake of brevity, suppose that these changes should concern only classes that have already been declared.
 ... 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()) { } } ... 
If engine classes have constructors without parameters, then this should also be used by adding constraint new () to the EngineT type parameter.
 ... 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. Add more diversity

Suppose now that there are only three engines in this example (yes, exactly three copies, one for each type), and there are many vehicles, and each of them needs to move one of the working engines from the warehouse. This is a good example from the point of view of the visibility of the changes occurring in the code, but it is a disgusting point of view of the logic of the real world: to rearrange the engine from one machine to another is a very dubious pleasure. Yes, and the consequences of such an architectural solution can be most surprising. But how will the code change, adapting to the new requirements:
 ... 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)]; } } ... 
As soalexmn accurately noted, for which he thanks, we just observed the Service locator pattern. As you can see, the farther into the forest, the less is left of the strategy.

5. Is it possible to share one engine for several cars?

Yes, of course you can. But here it is better to use a slightly different example, otherwise the fantasy simply explodes, trying to imagine a slave spinning the bicycle pedal with one foot, and kicking the plane with the second foot in an attempt to make it fly.
 I 1 = new (); .(1); .(1); .(1); 


I would be glad if these notes will be useful to anyone.

Source: https://habr.com/ru/post/322762/


All Articles