views:

302

answers:

5

I have an application which supports multiple types and versions of some devices. It can connect to these devices and retrieve various information.

Depending on the type of the device, I have (among other things) a class which can contain various properties. Some properties are common to all devices, some are unique to a particular device.

This data is serialized to xml.

What would be a preferred way to implement a class which would support future properties in future versions of these devices, as well as be backwards compatible with previous application versions?

I can think of several ways, but I find none of them great:

  • Use a collection of name-value pairs:
    • pros: good backward compatibility (both xml and previous versions of my app) and extensibility,
    • cons: no type safety, no intellisense, requires implementation of custom xml serialization (to handle different value objects)
  • Create derived properties class for each new device:
    • pros: type safety
    • cons: have to use XmlInclude or custom serialization to deserialize derived classes, no backward compatibility with previous xml schema (although by implementing custom serialization I could skip unknown properties?), requires casting for accessing properties in derived classes.
  • Another way to do it?

I am using C#, by the way.

+1  A: 

How about a something similar to a PropertyBag ?

AB Kolan
Well, that's is basically a collection of name-value pairs, I don't care for internal implementation so much (a list, dictionary, whatever). But it has the same cons.
Groo
This is what we used at the end, thanks!
Groo
A: 

I believe that creating derived properties is the best choice.

You can design your new classes using xml schema. And then just generate the class code with xsd.exe.

With .net isn't hard to develop a generic class that can serialize and deserialize all types to and from xml.

public static String toXmlString<T>(T value)
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
    StringWriter stringWriter = new StringWriter();
    try { xmlSerializer.Serialize(stringWriter, value); }
    catch (Exception e)
    {
        throw(e);
    }
    finally { stringWriter.Dispose(); }
    String xml = stringWriter.ToString();
    stringWriter.Dispose();
    return xml;
}

public static T fromXmlFile<T>(string fileName, Encoding encoding)
{
    Stream stream;
    try { stream = File.OpenRead(fileName); }
    catch (Exception e)
    {

      e.Data.Add("File Name", fileName);
      e.Data.Add("Type", typeof(T).ToString());
      throw(e);
    }

    BufferedStream bufferedStream = new BufferedStream(stream);
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));

    TextReader textReader;
    if (encoding == null)
        textReader = new StreamReader(bufferedStream);
    else
        textReader = new StreamReader(bufferedStream, encoding);

    T value;
    try { value = (T)xmlSerializer.Deserialize(textReader); }
    catch (Exception e)
    {
        e.Data.Add("File Name", fileName);
        e.Data.Add("Type", typeof(T).ToString());
        throw(e);
    }
    finally
    {
        textReader.Dispose();
        bufferedStream.Dispose();
    }
    return value;
}
Ricardo
The problem is backward compatibility - you cannot preserve it with XmlSerializer, because it's created for a specific type. If you set a property to an instance of a derived class, you will need to add [XmlInclude()] to specify all derived classes which XmlSerializer might encounter.
Groo
A: 
Peter
+1  A: 

If you're not limited to interoperability with an external schema, then you should use Runtime Serialization and the SoapFormatter. The pattern for runtime serialization permits derived classes to specify which of their properties need to be serialized and what to do with them when deserialized.

The XML Serializer requires XmlInclude because, in effect, it needs to define the schema to use.

John Saunders
Thanks, that may work better. But I would have to see how much it would take to remove XmlSerializer specific stuff and replace them with Runtime Serialization attributes and do all necessary changes. It's also interesting because I could serialize immutable types easily (right now I have to implement IXmlSerializable all the time and do it through reflection).
Groo
+1  A: 

I like name/value sets for this sort of thing.

Many of your cons can be dealt with -- consider a base class that acts as a general name/value set with no-op methods for validating incoming name/value pairs. For known sets of names (i.e. keys), you can create derived classes that implement validation methods.

For example, Printer may have a known key "PrintsColor" that can only be "true" or "false". If someone tries to load PrintsColor = "CMYK", your Printer class would throw an exception.

Depending on what you're doing, you can go a few different ways in terms of making the validation more convenient -- utility methods in the base class (e.g. checkForValidBoolean()) or a base class that accepts name/type information in its constructor for cleaner code in your derived classes, and perhaps a mostly automated XML serialization.

For intellisense -- your derived classes could have basic accessors that are implemented in terms of the key lookup. Intellisense would present those accessor names.

This approach has worked well for me -- there's sort of a short-sightedness to classic OO design, especially for large systems with plugged-in components. IMO, the clunkier type checking here is a big of a drag, but the flexibility make it worthwhile.

Thanks. I would still have to do some custom serialization with XmlSerializer, but at least I would get some run-time checking. And it's backward compatible with all previous app versions, which is also a great thing. I would prefer having a bunch of name-value derived classes which would do the validation, otherwise I would need to put some kind of a dictionary to lookup for the right delegate (predicate) by key inside the parent properties class (if I didn't miss something here?). And I would avoid putting a bunch of methods in the parent class (checkForThis, checkForThat).
Groo