Serializacja obiektów typu singleton\NULL object

Czasami zachodzi potrzeba serializacji obiektów, które powinny mieć  maksymalnie jedną kopie w tym samym AppDomain. Klasycznym przykładem jest System.DBNull, którego deklaracja wygląda następująco:

[SerializableAttribute]
[ComVisibleAttribute(true)]
public sealed class DBNull : ISerializable, 
    IConvertible

Załóżmy, że mamy klasę, w której jedna z właściwości ma wartość System.DBNull. Oczywiście podczas serializacji i potem deserializacji nie chcemy tworzyć nowej instancji DBNull – jest to sprzeczne z założeniem wzorca singleton albo special case pattern.

Na szczęście  istnieje sposób w .NET na prawidłową serializację singletów ale trzeba najpierw zaimplementować interfejs ISerializable. Zacznijmy jednak od przykładu, który pokaże nam jeszcze raz problem:

[Serializable]
public sealed class Singleton
{
   private static readonly Singleton _instance = new Singleton();
   private Singleton() { }

   public static Singleton Instance { get { return _instance; } }  
}    
class Program
{
   static void Main(string[] args)
   {
       BinaryFormatter serializer = new BinaryFormatter();
       
       Singleton singleton1 = Singleton.Instance;

       using (MemoryStream stream = new MemoryStream())
       {
           serializer.Serialize(stream,singleton1);
           stream.Position = 0;
           Singleton singleton2 = (Singleton)serializer.Deserialize(stream);
           Debug.Assert(ReferenceEquals(singleton1,singleton2));
       }
   }
}

Powyższy assert zakończy się niepowodzeniem ponieważ po serializacji stworzony zostanie nowy obiekt. W celu rozwiązania problemu najpierw trzeba zaimplementować interfejs ISerializable, który umożliwia własną serializację:

[Serializable]
public sealed class Singleton:ISerializable
{
   private static readonly Singleton _instance = new Singleton();
   private Singleton() { }

   public static Singleton Instance { get { return _instance; } }

   public void GetObjectData(SerializationInfo info, StreamingContext context)
   {
       info.SetType(typeof(SingletonSerializationHelper));
   }
}

W metodzie GetObjectData możemy umieścić kod odpowiedzialny za serializację. W naszym przypadku chcemy wywołać tylko metodę SetType, która deleguję serialziację do innej klasy. Innymi słowy, Serializer zamiast zapisywać obiekt Singleton, przejdzie do SingletonSerializationHelper. Przyjrzyjmy się teraz SingletonSerializationHelper:

[Serializable]
public sealed class SingletonSerializationHelper:IObjectReference
{
   public object GetRealObject(StreamingContext context)
   {
       return Singleton.Instance;
   }
}

Interfejs IObjectRefernece oznacza, że obiekt, który jest właśnie zapisywany, stanowi tak naprawdę referencję na inny obiekt. Po uruchomieniu przykładu, asercja zakończy się sukcesem. Microsoft jednak poleca aby wszelkie implementacje ISerializable wyglądały następująco:

[Serializable]
public sealed class Singleton:ISerializable
{
   private static readonly Singleton _instance = new Singleton();
   private Singleton() { }

   public static Singleton Instance { get { return _instance; } }

   [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
   public void GetObjectData(SerializationInfo info, StreamingContext context)
   {
       info.SetType(typeof(SingletonSerializationHelper));
   }
} 

W powyższym kodzie dodałem tylko atrybut SecurityPermissionAttribute. Oczywiście serializacja wyłącznie singletona nie ma żadnego sensu ponieważ po co zapisywać coś, co jest zawsze takie same? W praktyce jednak często pola innych obiektów zawierają referencję np. do wspomnianego System.DBNull. Jeśli implementujemy własny Special Case pattern, warto rozważyć również interfejs ISerializable jeśli serializacja wchodzi w grę.

Leave a Reply

Your email address will not be published.