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!!!

5 comentarios sobre “Desacopla tus datos XML del formato…”

  1. Hola Edu,

    acabo de ver tu post, muy interesante, pero voy a plantear la siguiente duda a ver que tal.

    Imaginemos que tenemos una clase base abstracta y genérica que nos ofrece las implementaciones de searializar y deserializar (si quieres también las de leer cargar a partir de un fichero y guardar en un fichero) recibiendo un párametro T. Cómo podría ser tu clase lector, pero que no lo queremos como un helper sino que nuestras clases TweeterInfo hereden de ella.

    ¿Que pasaría entonces con la clase TweeterInfo? Deberia heredar de esta clase base y además implementar la interfaz ITwitterInfo, pero ahora si queremos desacoplar nuestra clase allí donde se espere un ITwitterInfo no puede llegar un TweeterInfo, porque la interfaz ITwitterInfo no tiene los métodos de nuestra clase base. ¿Tendria la classe ITwitterInfo definir también los métodos de serializar/deserializar de nuestra clase base?

    Saludos,

  2. @Jordi! Que tal??? xD
    MMMmmm….

    Respuesta Räpida: Aunque la interfaz ITwitterInfo NO tenga los métodos de la clase base, donde se espere ITwitterInfo PUEDE pasarse TweeterInfo, sin ningún problema. Lo que no puedes es usar, desde ese sitio, los métodos de la clase base. Si deseas usarlo el parámetro debes declararlo como de la clase base o mejor como un interefaz que tenga las funciones de serialización o si quieres como un interfaz que tenga tanto serialización + ITwitterInfo. En todos los casos puedes pasar TweeterInfo, puesto que la clase implementa tanto la interfaz de serialización (via la clase base) e ITwitterInfo directamente.

    Respuesta Larga (un poco rollo)…
    Imagina que declaro una interfaz tal que así:

    interface ILectorXml
    {
    ITwitterInfo LeerFormato(string file);
    ITwitterInfo LeerFormato(string file, Type serType);
    }

    Esta interfaz me sirve para leer ITwitterInfo desde una fuente. Ahora puedo declarar la clase base que tu decías:

    public class BaseLectora : ILectorXml
    where TSer : class, ITwitterInfo
    {
    public ITwitterInfo LeerFormato(string file)
    {
    // …
    }
    public ITwitterInfo LeerFormato(string file, Type serType)
    {
    // …
    }
    }

    Bien, ahora si quieres nuestra clase TwitterInfo ya puede derivar de BaseLectora

    public class TweeterInfo : BaseLectora, ITwitterInfo
    {
    }

    En este punto si quiero puedo hacer:
    ITwitterInfo f5 = new TweeterInfo().LeerFormato(«Formato2.xml»);

    Fíjate pero en un tema: DEBO asignar el valor de LeerFormato a OTRO objeto. Por que? Pues bien, cuando implemento el método de LeerFormato() en la clase base no tengo acceso al setter de las propiedades. Este es el código de LeerFormato:

    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;
    }

    Evidentemente hay maneras de evitar eso (p.ej. declarar un método protegido abstracto llamado FillData() y que el método LeerFormato() en lugar de devolver el objeto leído llame a FillData() y este método «rellene los campos» de la clase). Entonces la clase TwitterInfo implementaría FillData() y «copiaria» la información del objeto leído en sus campos.

    Sobre lo de si ITwitterInfo debe declarar los métodos definidos en ILectorXml (es decir si ITwitterInfo debe derivar de ILectorXml) es algo que depende de si en un mismo sitio debes leer objetos + procesar sus campos. De todos modos que ITwitterInfo derive de ILectorXml no afecta a tus clases.

    Bueno… después del rollo mi opinión personal: No me gusta para nada esa jerarquía. Por varias razones, pero la más importante es que el hecho de que el método LeerFormato de la clase «base» devuelve ya un objeto que luego debes asignar a un objeto nuevo (como en mi ejemplo, cosa que NO es muy natural) o bien clonar en this (lo que seria el caso del FillData que he comentado antes). Esto no es una buena solución…

    … El problema es que la clase base NO implementa ITwitterInfo (ni PUEDE hacerlo, puesto que queremos derivar esta implementación en cada clase hija), así que lo único que puedes hacer es devolver un objeto nuevo de un tipo concreto (el tipo concreto si que lo sabes). Si la interfaz ITwitterInfo declarase setters públicos todavía podrías hacer algo ya que podrías acceder a las propiedades, pero estás obligando a tener setters sólo para solucionar tus problemas con una jerarquía de clases.

    En fin… no se si ha quedado muy claro… 🙂

    Saludos y gracias por comentar!! 😉

Responder a jtorrecilla Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *