Применение паттерна CRTP в C#

CRTP (Curiously recurring template pattern) — идиома, ведущая свои корни из C++. Суть CRTP заключается в наследовании от шаблонного (generic) класса, шаблонным параметром которого является сам класс-наследник.

В коде это выглядит достаточно просто:

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 и обратно, в случае, если использование автоматических мэпперов является бутылочным горлышком в производительности.

ссылка на оригинал статьи http://habrahabr.ru/post/211743/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *