miércoles, 26 de mayo de 2010

Desacopla tus datos XML del formato…

Leyendo este post de Gisela sobre la serialización XML me he decidido escribir este… es lo que tiene la realimentación en los blogs :)

El uso de atributos que menciona Gis en su post es realmente genial. A mi me encanta: me permite definir mis clases en un momento y es muy útil cuando leemos datos xml de una fuente externa. Pero hay un detalle que puede ser un problema: El esquema XML está totalmente acoplado de la clase que tiene los datos. Si estamos leyendo de dos fuentes externas que tienen esquemas XML distintos pero tienen los mismos datos, debemos duplicar las clases que serializan esos datos, ya que los atributos a usar serán distintos.

P.ej. supon que tenemos dos fuentes externas, que nos devuelven los mismos datos, pero de forma diferente:

<info>
<usuarios>
<usuario>
<nombre>eiximenis</nombre>
<nombrereal>Edu</nombrereal>
</usuario>
</usuarios>
</info>






<information>
<twitter.users>
<twitter.user name="eiximenis" realname="Edu" />
</twitter.users>
</information>





La información es exactamente la misma, pero el esquema es distinto. Si queremos usar atributos para leer estos dos archivos xml debemos implementar dos conjuntos de clases:




// Clases para leer el 1er formato
[XmlRoot("info")]
public class Info
{
private readonly List<UserInfo> _users;

public Info()
{
_users = new List<UserInfo>();
}

[XmlArray("usuarios")]
[XmlArrayItem("usuario", typeof(UserInfo))]
public List<UserInfo> Usuarios { get { return _users; } }
}

public class UserInfo
{
[XmlElement("nombre")]
public string Nombre { get; set; }

[XmlElement("nombrereal")]
public string NombreReal { get; set; }
}






// Clases para leer el segundo formato
[XmlRoot("information")]
public class TweeterInfo
{
private readonly List<TweeterUserInfo> _users;

public TweeterInfo()
{
_users = new List<TweeterUserInfo>();
}

[XmlArray("twitter.users")]
[XmlArrayItem("twitter.user", typeof(TweeterUserInfo))]
public List<TweeterUserInfo> Users { get { return _users; } }
}

public class TweeterUserInfo
{
[XmlAttribute("name")]
public string Name { get; set; }

[XmlAttribute("realname")]
public string RealName { get; set; }
}





El problema no es que tengamos que realizar el doble de trabajo… es que tenemos dos conjuntos de clases totalmente distintos que no tienen ninguna relación entre ellos. Si desarrollamos un método que trabaje con objetos de la clase Info, dicho método no trabajará con objetos de la clase TweeterInfo aún cuando ambas clases representan la misma información.



La solución pasa, obviamente, por usar interfaces: Ambas clases deberían implementar una misma inferfaz que nos permitiese acceder a los datos:




// Interfaces

public interface ITwitterInfo
{
IEnumerable<ITwitterUser> Users { get; }
}
public interface ITwitterUser
{
string Name { get; }
string RealName { get; }
}





Las interfaces se limitan a definir los datos que vamos a consultar desde nuestra aplicación. En este caso sólo hay getters porque se supone que dichos datos son de lectura sólamente.



El siguiente paso es implementar dichas interfaces en nuestras clases. Lo mejor es usar una implementación explícita. La razón es evitar tipos de retorno distintos entre propiedades que pueden tener el mismo nombre (p.ej. la propiedad Users de TweeterInfo devuelve una List<TweeterUserInfo> mientras que la propiedad de la interfaz ITwitterInfo devuelve un IEnumerable<ITwitterUser> y eso no compilaría:




[XmlRoot("information")]
public class TweeterInfo : ITwitterInfo
{
private readonly List<TweeterUserInfo> _users;

public TweeterInfo()
{
_users = new List<TweeterUserInfo>();
}

[XmlArray("twitter.users")]
[XmlArrayItem("twitter.user", typeof(TweeterUserInfo))]
public List<TweeterUserInfo> Users { get { return _users; } }
}

// error CS0738: 'XmlDesacoplado.Formato2.TweeterInfo' does not implement interface member
// 'XmlDesacoplado.Interfaz.ITwitterInfo.Users'. 'XmlDesacoplado.Formato2.TweeterInfo.Users' cannot
// implement 'XmlDesacoplado.Interfaz.ITwitterInfo.Users' because it does not have the matching return type
// of 'System.Collections.Generic.IEnumerable<XmlDesacoplado.Interfaz.ITwitterUser>'





Como os digo, para que funcione basta con una implementación explícita de la interfaz. Es decir basta con añadir:




// Implementación explícita
IEnumerable<ITwitterUser> ITwitterInfo.Users { get { return Users; } }





En el resto de clases debemos hacer lo mismo (implementar las interfaces).



Ahora todo nuestro código puede trabajar simplemente con objetos ITwitterInfo.



Vale… esto está muy bien, pero como puedo cambiar dinámicamente el “formato” del archivo a leer?



Una posible solución es realizar una clase “lectora” de XMLs, algo tal que así:




class LectorXmls
{
public ITwitterInfo LeerFormato<TSer>(string file) where TSer : class, ITwitterInfo
{
TSer data = default(TSer);
using (FileStream fs = new FileStream(file, FileMode.Open))
{
XmlSerializer ser = new XmlSerializer(typeof(TSer));
data = ser.Deserialize(fs) as TSer;
}
return data;
}
}





Y luego en vuestro código podéis escoger que clase serializadora usar:




ITwitterInfo f1 = new LectorXmls().LeerFormato<Info>("Formato1.xml");





Por supuesto, si quieres que el tipo de la clase serializadora esté en un archivo .config y así soportar “futuros formatos” de serialización no podrás usar un método genérico, pero en este caso te basta con pasar un Type como parámetro:




public ITwitterInfo LeerFormato(string file, Type serType)
{
ITwitterInfo data = null;
using (FileStream fs = new FileStream(file, FileMode.Open))
{
XmlSerializer ser = new XmlSerializer(serType);
data = ser.Deserialize(fs) as ITwitterInfo;
}
return data;
}





Y el valor del Type lo puedes obtener a partir de un fichero de .config.



Un saludo!!!



PD: Esto es un crosspost desde mi blog en geeks.ms!

No hay comentarios: