0,0
рейтинг
7 февраля 2014 в 13:53

Разработка → Применение паттерна CRTP в C# из песочницы

C#*, .NET*
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 и обратно, в случае, если использование автоматических мэпперов является бутылочным горлышком в производительности.
Дмитрий Разумихин @radium
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (9)

  • +1
    Как-то хотел применить такой подход: paste.org.ru/?x8y5qi — вот без компилятора, как вам кажется, всё окей?
  • +1
    var frame = new Frame()
    .SetWidth(100)
    .SetHeight(200)
    .SetColor(Color.White);

    А если появится наследник от Frame, то на SetColor(Color.White) все и закончится.
    • +1
      А почему? Из-за проблем с ковариантностью и контравариантностью?

      Вообще, это какая-то мудреная реализация. Не очень ясен ее исключительный профит.
    • 0
      Вы очень внимательны! Я думаю это просто опечатка.
    • 0
      Чтобы делать наследника от Frame, сам Frame должен быть реализован соответственно тому же самому паттерну:

      public class FrameImpl<Derived> : Rectangle<Derived>
      {
          Color _color;
      
          public Derived SetColor(Color color)
          {
              _color = color;
              return (Derived) this;
          }
      }
      


      Ну и потом:
      class Frame : FrameImpl<Frame>
      { /* здесь пусто, кроме может быть конструкторов */ };
      


      Как завещали наши прадеды в ATL 20 лет тому назад.
  • +1
    Я что-то не понял. А что мешало в SerializerBase методы сериализации списка сделать нестатическими?
  • +1
    Я такой способ применял для выноса в один из базовых классов методов, которые устарели. И макросом экранировал это наследование. Таким образом можно было собирать как с поддержкой обсолет\деприкейт, так и без нее. А сам класс оставался чистым.
  • +2
    Это сделано чтобы использовать GeoPointSerializer на манер хэлпера — через статические методы.

    Можно написать так:
    public abstract class SerializerBase<TSerializer, TItem> where TSerializer : SerializerBase<TSerializer, TItem>, new()
    {
        public abstract void WriteAsBinary(TItem item, BinaryWriter writer);
        
        public void Save(TItem item, BinaryWriter writer)
        {
            WriteAsBinary(item, writer);
        }
    
        public void Save(IList<TItem> items, BinaryWriter writer)
        {
            writer.Write(items.Count);
            foreach (var item in items)
                WriteAsBinary(item, writer);
        }
    
        public void Save(string name, TItem item, BinaryWriter writer)
        {
            writer.Write(name);
            WriteAsBinary(item, writer);
        }
    }
    

    И использовать через создание объекта:
    var gps = new GeoPointSerializer();
    gps.Save("Mount Kilimanjaro", gp, writer);
    gps.Save(region, writer);
    
  • 0
    При создании fluent интерфейсов с этот паттерн сам собой рождается)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.