
Applying the CRTP Pattern in C #
CRTP ( Curiously recurring template pattern ) is an idiom leading its roots from C ++. The essence of CRTP is to inherit from a generic class whose template parameter is the inheriting class itself.
In the code, it looks quite simple:
This approach allows you to operate with the type of the derived class (T) in the code of the base class, for example, explicitly cast this to type T.
Consider a couple of practical applications.
The first of these is the implementation of the Fluent interface in terms of class inheritance:
A clear example of the result:
The ability to call SetColor () is ensured by the fact that the SetWidth () / SetHeight () methods in this context return an object of the Frame class even when declared in the Rectangle base class.
The second option is to transfer generalized tasks to static methods of the base class. At the same time, the logic necessary for the work of these methods is implemented in the successor class.
Consider this using the TSerializer class as an example of serializing TItem elements:
SerializerBase is an abstract class declared with two template parameters, and TSerializer should be a class with a constructor without parameters derived from the SerializerBase itself. Inside, there is a static field containing the singleton object of the derived class created in the static constructor. Overloaded Save methods call the WriteAsBinary method on a singleton:
Thus, by implementing the serialization code of one element, we get the opportunity to serialize both the list of elements and arbitrary datasets with TItem through the static methods GeoPointSerializer.Save, which are inherited from the base class.
Usage example:
In this case, CRTP helps to effectively separate the serialization logic from the data itself and provides convenient access to methods. A similar solution can be useful for implementing business logic class mappers in the DTO and vice versa, if the use of automatic mappers is a bottleneck in performance.
In the code, it looks quite simple:
public class Base where T : Base
{ /* ... */ }
public class Derived : Base
{ /* ... */ }
This approach allows you to operate with the type of the derived class (T) in the code of the base class, for example, explicitly cast this to type T.
Consider a couple of practical applications.
The first of these is the implementation of the Fluent interface in terms of class inheritance:
public class Rectangle where T : Rectangle
{
int _width;
int _height;
public T SetWidth(int width)
{
_width = width;
return (T)this;
}
public T SetHeight(int height)
{
_height = height;
return (T)this;
}
}
public class Frame : Rectangle
{
Color _color;
public Frame SetColor(Color color)
{
_color = color;
return this;
}
}
A clear example of the result:
var frame = new Frame()
.SetWidth(100)
.SetHeight(200)
.SetColor(Color.White);
The ability to call SetColor () is ensured by the fact that the SetWidth () / SetHeight () methods in this context return an object of the Frame class even when declared in the Rectangle base class.
The second option is to transfer generalized tasks to static methods of the base class. At the same time, the logic necessary for the work of these methods is implemented in the successor class.
Consider this using the TSerializer class as an example of serializing TItem elements:
public abstract class SerializerBase where TSerializer : SerializerBase, new()
{
readonly static TSerializer _serializer;
static SerializerBase()
{
_serializer = new TSerializer();
}
public abstract void WriteAsBinary(TItem item, BinaryWriter writer);
public static void Save(TItem item, BinaryWriter writer)
{
_serializer.WriteAsBinary(item, writer);
}
public static void Save(IList items, BinaryWriter writer)
{
writer.Write(items.Count);
foreach (var item in items)
_serializer.WriteAsBinary(item, writer);
}
public static void Save(string name, TItem item, BinaryWriter writer)
{
writer.Write(name);
_serializer.WriteAsBinary(item, writer);
}
}
SerializerBase is an abstract class declared with two template parameters, and TSerializer should be a class with a constructor without parameters derived from the SerializerBase itself. Inside, there is a static field containing the singleton object of the derived class created in the static constructor. Overloaded Save methods call the WriteAsBinary method on a singleton:
public class GeoPoint
{
public double Lat { get; set; }
public double Lon { get; set; }
}
public class GeoPointSerializer : SerializerBase
{
public override void WriteAsBinary(GeoPoint item, BinaryWriter writer)
{
writer.Write(item.Lat);
writer.Write(item.Lon);
}
}
Thus, by implementing the serialization code of one element, we get the opportunity to serialize both the list of elements and arbitrary datasets with TItem through the static methods GeoPointSerializer.Save, which are inherited from the base class.
Usage example:
GeoPoint[] region = new GeoPoint[] {
new GeoPoint { Lat = 0.0, Lon = 0.0 },
new GeoPoint { Lat = -25, Lon = 135 },
new GeoPoint { Lat = -20, Lon = 46}
};
GeoPoint gp = new GeoPoint() { Lat = -3.065, Lon = 37.358 };
byte[] bytes;
using (MemoryStream ms = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(ms))
{
GeoPointSerializer.Save("Mount Kilimanjaro", gp, writer);
GeoPointSerializer.Save(region, writer);
bytes = ms.ToArray();
}
In this case, CRTP helps to effectively separate the serialization logic from the data itself and provides convenient access to methods. A similar solution can be useful for implementing business logic class mappers in the DTO and vice versa, if the use of automatic mappers is a bottleneck in performance.