CRTP (Curiously recurring template pattern) — идиома, ведущая свои корни из C++. Суть CRTP заключается в наследовании от шаблонного (generic) класса, шаблонным параметром которого является сам класс-наследник.
В коде это выглядит достаточно просто:
Такой подход позволяет оперировать типом класса-наследника (T) в коде базового класса, например, явно приводить this к типу T.
Рассмотрим пару вариантов практического применения.
Первый из них – это реализация Fluent-интерфейса в условиях наследования классов:
Наглядный пример результата:
Возможность вызова SetColor() обеспечивается тем, что методы SetWidth()/SetHeight() в данном контексте возвращают объект класса Frame даже будучи объявленными в базовом классе Rectangle.
Второй вариант заключается в выносе обобщённых задач в статические методы базового класса. При этом логика, которая необходима для работы этих методов, реализована в классе-наследнике.
Рассмотрим это на примере сериализации элементов TItem классом TSerializer:
SerializerBase – это абстрактный класс, объявленный с двумя шаблонными параметрами, причём TSerializer должен быть классом с конструктором без параметров производным от самого SerializerBase. Внутри имеется статическое поле, содержащее синглтон-объект класса-наследника, создаваемый в статическом конструкторе. Перегруженные методы Save вызывают у синглтона метод WriteAsBinary:
Таким образом, реализовав код сериализации одного элемента, мы получаем возможность сериализовать и список элементов, и произвольные наборы данных с участием TItem через статические методы GeoPointSerializer.Save, которые унаследованы от базового класса.
Пример использования:
В данном случае CRTP помогает эффективно отделить логику сериализации от самих данных и обеспечивает удобный доступ к методам. Подобное решение может быть полезным и для реализации мэпперов классов бизнес-логики в DTO и обратно, в случае, если использование автоматических мэпперов является бутылочным горлышком в производительности.
В коде это выглядит достаточно просто:
public class Base<T> where T : Base<T>
{ /* ... */ }
public class Derived : Base<Derived>
{ /* ... */ }
Такой подход позволяет оперировать типом класса-наследника (T) в коде базового класса, например, явно приводить this к типу T.
Рассмотрим пару вариантов практического применения.
Первый из них – это реализация Fluent-интерфейса в условиях наследования классов:
public class Rectangle<T> where T : Rectangle<T>
{
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<Frame>
{
Color _color;
public Frame SetColor(Color color)
{
_color = color;
return this;
}
}
Наглядный пример результата:
var frame = new Frame()
.SetWidth(100)
.SetHeight(200)
.SetColor(Color.White);
Возможность вызова SetColor() обеспечивается тем, что методы SetWidth()/SetHeight() в данном контексте возвращают объект класса Frame даже будучи объявленными в базовом классе Rectangle.
Второй вариант заключается в выносе обобщённых задач в статические методы базового класса. При этом логика, которая необходима для работы этих методов, реализована в классе-наследнике.
Рассмотрим это на примере сериализации элементов TItem классом TSerializer:
public abstract class SerializerBase<TSerializer, TItem> where TSerializer : SerializerBase<TSerializer, TItem>, 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<TItem> 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 – это абстрактный класс, объявленный с двумя шаблонными параметрами, причём TSerializer должен быть классом с конструктором без параметров производным от самого SerializerBase. Внутри имеется статическое поле, содержащее синглтон-объект класса-наследника, создаваемый в статическом конструкторе. Перегруженные методы Save вызывают у синглтона метод WriteAsBinary:
public class GeoPoint
{
public double Lat { get; set; }
public double Lon { get; set; }
}
public class GeoPointSerializer : SerializerBase<GeoPointSerializer, GeoPoint>
{
public override void WriteAsBinary(GeoPoint item, BinaryWriter writer)
{
writer.Write(item.Lat);
writer.Write(item.Lon);
}
}
Таким образом, реализовав код сериализации одного элемента, мы получаем возможность сериализовать и список элементов, и произвольные наборы данных с участием TItem через статические методы GeoPointSerializer.Save, которые унаследованы от базового класса.
Пример использования:
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();
}
В данном случае CRTP помогает эффективно отделить логику сериализации от самих данных и обеспечивает удобный доступ к методам. Подобное решение может быть полезным и для реализации мэпперов классов бизнес-логики в DTO и обратно, в случае, если использование автоматических мэпперов является бутылочным горлышком в производительности.