вторник, 12 августа 2008 г.

Трюк с generic-ами.

Сегодня покажу как можно одной строчкой прикрутить к классу некоторую функциональность с учетом типа класса. К сожалению, данный трюк имеет ограничения, но все же я им часто пользуюсь.

Приведу случай из реальной жизни. В системе, в разработке которой я принимаю участие, требуется довольно много разных данных сохранять в xml. Поначалу мы много использовали xml сериализацию. Требовалось уметь сериализовывать и десериализовывать 5 или 6 типов графов. Единственное, что было общее у этих графов - необходимость сериализации в файл, в строку. Вполне естевственно, что после появления второй иерархии объектов для сериализации, возникло желание использовать общий код для сериализации и десериализации графов. Приходила в голову идея использовать внешнюю утилиту, однако использование внешней утилиты показалось не очень красивым (требовалась передача типа корня сериализации во внешний метод):
Foo foo = XmlUtils.DeserializeFromFile(typeof(Foo), "Foo.xml");
или
Foo foo = XmlUtils.DeserializeFromFile<Foo>("Foo.xml");
а душе хотелось полета:
Foo foo = Foo.DeserializeFromFile("Foo.xml");
Таким образом, требуется определить статический метод, который бы для типа Foo возвращал результат типа Foo, а для типа Bar возвращал бы результат типа Bar. Есть средство. Определим хитрый базовый класс:
public class XmlSerializationRoot<TRoot>
   where TRoot : XmlSerializationRoot<TRoot>
{
   public static TRoot DeserializeFrom(TextReader reader)
   {
       if (reader == null)
           throw new ArgumentNullException("reader");

       return (TRoot)CreateSerializer().Deserialize(reader);
   }

   public static TRoot DeserializeFrom(Stream stream)
   {
       if (stream == null)
           throw new ArgumentNullException("stream");

       using (StreamReader reader = new StreamReader(stream))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromString(string xml)
   {
       using (TextReader reader = new StringReader(xml))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromFile(string fileName)
   {
       using (TextReader reader = File.OpenText(fileName))
           return DeserializeFrom(reader);
   }

   private static XmlSerializer CreateSerializer()
   {
       return new XmlSerializer(typeof(TRoot));
   }

   public void SerializeTo(TextWriter writer)
   {
       if (writer == null)
           throw new ArgumentNullException("writer");

       CreateSerializer().Serialize(writer, this);
   }

   public string SerializeToString()
   {
       StringBuilder builder = new StringBuilder();
       using (StringWriter writer = new StringWriter(builder))
           this.SerializeTo(writer);

       return builder.ToString();
   }

   public void SerializeToFile(string fileName)
   {
       using (StreamWriter writer = File.CreateText(fileName))
           this.SerializeTo(writer);
   }

   public void SerializeTo(Stream stream)
   {
       using (StreamWriter writer = new StreamWriter(stream))
           this.SerializeTo(writer);
   }
}
Особый интерес представляют статические методы десериализации. Методы сериализации были внесены до кучи, чтобы пример был полным. Вообще говоря, класс XmlSerializationRoot не будет являться базовым классом для классов с возможностью сериализации. Базовым классом для них будет XmlSerializationRoot<T>. Т.е. при следующем объявлении класса Foo
class Foo : XmlSerializationRoot<Foo>
{
}
мы получим ситуацию, где класс Foo наследует определенные у класса XmlSerializationRoot<Foo> методы. Теперь можно пользоваться этими методами через идентификатор типа Foo:
Foo foo = Foo.DeserializeFromFile("foo.xml");
В качестве завершающего штриха предлагаю выделить интерфейс
public interface IXmlSerializable
{
   void SerializeTo(TextWriter writer);
   void SerializeToFile(string fileName);
   void SerializeTo(Stream stream);
   string SerializeToString();
}
и поддержать его классом XmlSerializationRoot. Теперь мы сможем вызывать методы сериализации не зная типа сохраняемого объекта. Теперь буду писать гадости про этот подход.
  1. Данный подход навязывает ограничение наследования на разрабатываемые классы. При необходимости наследования классов от другого класса, использование усложняется, однако, можно объявить собственные методы и делегировать их вспомогательному классу. Например:
    class Foo : SomeBaseClass // наследуемся от чего-то другого
    {
       public static Foo DeserializeFromFile(string fileName)
       {
           return XmlSerializationRoot<Foo>.DeserializeFromFile(fileName);
       }
    }
    В данном случае потребуется убрать constraint у класса XmlSerializationRoot. В принципе, он объявлен формально для контроля за способом использования функциональности, и не требуется для компиляции методов класса.
  2. Resharper ругается на использование методов базового класса через идентификатор производного типа. Можно подкрутить его настройки, вставить управляющий комментарий, либо просто игнорировать данные сообщения.
  3. Пользуясь этим подходом мы не можем прочитать что-то из файла, а потом разобраться, что это было. Но в этом виноват не только сей подход, а так же устройство XML сериализации в FCL.
  4. Если требуется объявить только статические методы, то мы не сможем объявить базовый класс с модификатором static. Если объявим, то не сможем унаследовать от него.
Хоть область применения данного подхода не широка, но иногда это то что надо. Например, при необходимости подсчета экземпляров классов разного типа.

P.S. Хочу обратить внимание, что в данном посте не шла речь о корректности данного подхода к сериализации в архитектурном плане. Это лишь пример издевательства над языком. Однако, данный код отлично работал и был удобно используемым, пока мы не перешли на более тяжелый (но более гибкий) способ сериализации через XmlDocument.

Комментариев нет:

Отправить комментарий