3

The data of my application is retrieved from an XML file. XML file is versioned, but there is a new XML version file very often. The structure of each XML file changes with respect to the other versions. Sometimes the changes are minor, sometimes are big.

For example, if version XMLFileV1 contains something like:

<Shape>
  <Id>0</Id>
  <Features>
    <Feature>
      <Name>Name0</Name>
      <Color>Color0</Color>
    </Feature>
    <Feature>
      <Name>Name00</Name>
      <Color>Color00</Color>
    </Feature>
  </Features>
</Shape>

The content of FileXMLV2 for the same "Shape" element may be like:

<Shape>
  <SubShapes>
    <Subshape>
      <Id>0</Id>
      <Code>00</Code>
    </Subshape>
    <Subshape>
      <Id>1</Id>
      <Code>01</Code>
    </Subshape>
  <SubShapes>
  <Features>
    <Feature>
      <Name>Name0</Name>
      <Color>Color0</Color>
    </Feature>
    <Feature>
      <Name>Name00</Name>
      <Color>Color00</Color>
    </Feature>
  </Features>
</Shape>

Application must be capable to read and handle any version of XML file, not only the current one.

For the situation shown above is difficult to have an stable "Shape" class that can be serialized/deserialized with all versions of XML files.

I thought of following options to deal with such situation:

  1. Create a base class of Shape that correspond with the oldest version of XML file. Then, for let's say XMLFileV1, create ShapeV1 that derivates from the Shape and add the needed changes to this ShapeV1 according to XMLFileV1. So, when XMLfileV2 arrives and element is modified again, create a new ShapeV2 that derivates from ShapeV1 (or from base Shape if needed) to be accorded to the new XMLFileV2 requirements. So that the serialization/deserialization could be possible because XML file would be "mapped" exactly with the corresponding "Shape" object.
  2. Create only one Shape class with the form of the current version of XML file. So when application reads an older version of XML File, it would left in blank the fields of Shape class that does not exist in older XML file. When a version newer than the current one arrives, I would just modify the Shape class to add the needed fields. I won't be able to serialize/deserialize Shape class as in option 1, but I would read the XML "line-by-line" stlye (using XMLReader in C# for example).

So my questions are:

  • which of above approach is better to maintain in the long time?
  • Do you have approach better that both I have presented?

Thanks!

1
  • I'd escalate this issue with management because I wouldn't want to spend my time dealing with unexpected file format changes. This is ridiculous.
    – Dan Wilson
    Mar 25, 2020 at 22:22

4 Answers 4

1

Creating classes for different versions (approach 1) doesn't scale that well. If there will be many versions to come, you end up with a class inflation which is never a good thing. And then, what if there will be ShapeV3, that can not be modeled as a subclass of V2, but just of a subclass of V1 or the base class, yet it offers similar features than V2? Then you can as well create all these classes without any inheritance and just end up with a bunch of classes that don't even share a common interface.

So I'm rather for approach 2 but I don't quite understand your implementation approach. I would make a Shape class, that can store everything that any Shape ever has to store and then write importers and exporters for the different versions, which don't have to be own classes, they can as well just be functions or static methods of a single class.

To make an analogy: Consider you are getting image files sent that you need to work with. They could be PNG or JPEG or BMP, but tomorrow they could also be WebP. So you just create a class named Image that stores image data and then you write importers that can read the individual file formats and emit an Image object as well as exporters that can export an Image object to a specific file formats.

1

There are a couple of alternatives. First of all, objects should not change just because their data changed. That is just a basic consequence of basic concepts object-orientation. One of which is that objects are defined by behavior, and data is encapsulated under that behavior.

  1. You could define your Shape to have the behavior you need, like drawing on a canvas, calculating area, or whatever. This way you don't have to change it when the wire-format changes, you just have to extend the parser, or just offer a new parser method in it.

  2. You can have the Shape be an interface. Still defining only the behavior, not the data. And then have different implementations based on the different wire-formats. This may be better, if the different format shapes behave also slightly differently.

The system will be much more maintainable, if you don't propagate the data but hide it under proper behaviors. As a side-effect it will be much more object-oriented as well.

1

It all depends on what your Shape is used for!

Short Answer

Robert Bräutigam's answer pretty much nails it. This is technically the best you can do. The Shape class does not need to expose too much, definitely not all of its members. That is, if you are using the Shape class. For all we know, you may be using the Shape just for the purpose of representation, so you need all these properties to be visible somehow, somewhere. As far as this matter is concerned, we are dealing with an X-->Y problem, so we would need more details.

(Too?) Long Answer

Think of your application as the sales department of an international company. Employees are Shapes (I know, I know, bear with me please). You people need to manage sales across the world. Unfortunately, all collaborating clients require interaction at their own location and communication in their own language (I'm sorry, your requirements correspond to such strict rules in the analogy)! Your given options, then, represent the following scenarios:

  • Option 1 (different class versions for a base Shape class). An important sale comes up in Japan. In your sales department, nobody speaks Japanese. You need to (and do) find someone speaking Japanese (and is a seasoned salesperson) and send them to do the job! In short, your Option 1 corresponds to the sales department sending a person who speaks the language of the country you are selling to. This means you need to have as many people, as are the languages you may deal with. It doesn't matter much if an employee speaks 3 languages fluently...they cannot be in 3 places at the same time (ha, gotcha!).

  • Option 2 (only one Shape class that is re-adapted in time). An important sales comes up in France. You need to go there, you have understood, by now, that top sales representatives fluent in the language you happen to need don't really come a dime a dozen. But the sales department does have great sales representatives. So, you arrange intensive courses in French, 1 month, just to get around the basics, customers will understand, they only care about the product, after all. Great sales representative born in the Netherlands now speaks some French. Next month, they need to speak some German too (oh, they already speak a lot of German, OK, make that Finnish).

If you understand where I'm getting at, neither option represents optimal resource management. The core problem your options are ignoring is that your application will have to survive in computers other than your own. Updating the source code every now and then means you have to compile your code again, deploy your code again and... are you really prepared to ship a new version of your entire application (OK, maybe only some module) every time a file format changes? Well, you may be, but this can quickly become a problem and is definitely not the greatest method to manage your resources.

Enter

  • Option 3: Use a dictionary and/or a translator! Yes, hire someone from the country you will go to, either temporarily or permanently, they will mediate the communication and you will have much more success in much less time, with much fewer costs involved than the case of having your employees learn a new language every now and then (which is, well, not as easy as I may have made it seem above, frankly).

So, because your application will have to survive in a faraway land, you might want to use an additional "helper" dictionary. An "instructional" XML (sort of like a schema) map. This accompanying XML will contain information about what is parsed and what it ends up being assigned to. Check the following (naive) example in c#.NET:

Your classes:

class PropertyObject
{
   //Stuff that MAY become necessary in the future.
    Dictionary<string, string> m_Properties;
    Dictionary<string, Type> m_PropertyTypes;

    List<PropertyObject> m_NestedObjects;
}

class Shape : PropertyObject
{
    //Necessary stuff that you KNOW should exist!
    List<Feature> m_Features;
    string m_Id;
    //etc..
}

class Feature : PropertyObject
{
    string m_Name;
    Color m_Color;
}

Your XML:

<Shape>
  <Id>0</Id>
  <Features>
    <Feature>
      <Name>Name0</Name>
      <Color>Color0</Color>
    </Feature>
    <Feature>
      <Name>Name00</Name>
      <Color>Color00</Color>
    </Feature>
  </Features>
</Shape>

Your potential map:

<xs:Shape>
    <member id="Id" mapsTo="m_id" type="string"/>
    <child id="Features" mapsTo="m_Features" type="Feature"/>
</xs:Shape>
<xs:Feature>
    <member id="Name" mapsTo="m_name" type="string"/>
    <!-- You might like to include full namespaces in some cases, for example... -->
    <member id="Color" mapsTo="m_Color" type="System.Drawing.Color"/>
</xs:Feature>

Now any changes that may come up will necessitate that you only change the map, not the entire assembly/application. Consider the following example, a property comes up, useless to you, for the time being:

Your new XML:

<Shape>
  <Id>0</Id>
  <Gender>Female</Gender>
  <Features>
    <Feature>
      <Name>Name0</Name>
      <Color>Color0</Color>
    </Feature>
    <Feature>
      <Name>Name00</Name>
      <Color>Color00</Color>
    </Feature>
  </Features>
</Shape>

Your new Map:

<xs:Shape>
    <member id="Id" mapsTo="m_id" type="string"/>
    <child id="Features" mapsTo="m_Features" type="Feature"/>
    <general id="Gender" type="string"/>
</xs:Shape>
<xs:Feature>
    <member id="Name" mapsTo="m_name" type="string"/>
    <!-- You might like to include full namespaces in some cases, for example... -->
    <member id="Color" mapsTo="m_Color" type="System.Drawing.Color"/>
</xs:Feature>

And you will have taken the proper care to parse fields mapped as general, for example, to the m_Properties dictionary as simple strings, and their types (IF they exist, otherwise you can consider them strings). Someone else might prefer <ignore id="Gender"/> so that you can just skip irrelevant stuff. Of course, if new types make their appearance, you may have to recompile, but there might be steps in that direction, which you could take to mitigate this problem as well. You can even store the entire XML within the object and parse it dynamically.

I am probably oversimplifying (or over-engineering) this, but, in general, I am trying to give you a conceptual direction. Look into any potentially already available tools and try to use an external mapping that is configurable in isolation. When your requirements change very often, instead of constantly Shape-shifting to adapt, just introduce an additional layer of abstraction.

0

I would probably look into using dynamics. You could even do a hybrid type, with the version 1.0 - or the base-version-with-never-to-be-changed properties - coded as usual and any changes implemented down the road handled by dynamic, the path of least resistance being an implementation using ExpandoObject.

This should be future proof, allowing new properties to be added runtime, both single properties, lists and complex nested properties. To really sweeten the deal, you get:

  1. When data-binding the static and dynamic properties are merged, making it appear as a regular type (though, you don't get IntelliSense on dynamic properties of course)
  2. INotifyPopertyChanged is thrown in for free, with a one-line implementation
  3. You can also add methods and events runtime

Pseudo coded example, you will probably have to change some bits to make it work:

public class DynamicXmlDataObject
{
    public static DynamicXmlDataObject Create(XElement xmlData)
    {
        var newDataObject = new DynamicXmlDataObject(xmlData);
        newDataObject.ParseStaticData(null, xmlData);
        newDataObject.ParseDynamicData(null ,xmlData);
        return newDataObject;
    }

    private DynamicXmlDataObject(XElement xmlData)
    {
        TimeCreated = DateTime.Now;
        Name = xmlData.Name.LocalName;
        XmlElements = xmlData.DescendantsAndSelf();
        DynamicProperties = new ExpandoObject();
    }

    public DateTime TimeCreated { get; }
    public string Name { get; }
    public string Version { get; }

    public string Whatever { get; private set; }
    public string AndSoOn { get; private set; }

    public IEnumerable<XElement> XmlElements { get; }
    private ExpandoObject DynamicProperties { get; }

    private void ParseStaticData(dynamic parent, XElement xmlData)
    {
        //Do stuff like...
        Whatever = xmlData.DescendantsAndSelf(nameof(Whatever)).FirstOrDefault().Value;
        AndSoOn = xmlData.DescendantsAndSelf(nameof(AndSoOn)).FirstOrDefault().Value;
    }

    private void ParseDynamicData(dynamic parent, XElement xmlData)
    {
        //Do something like...
        if (xmlData.HasElements)
        {
            if (ElementIsCollection(xmlData))
            {
                var item = new ExpandoObject();
                var list = new List<dynamic>();
                foreach (var element in xmlData.Elements())
                {
                    ParseDynamicData(list, element);
                }

                AddProperty(item, xmlData.Elements().First().Name.LocalName, list);
                AddProperty(parent, xmlData.Name.ToString(), item);
            }
            else
            {
                var item = new ExpandoObject();
                foreach (var attribute in node.Attributes())
                {
                    AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim());
                }

                foreach (var element in xmlData.Elements())
                {
                    ParseDynamicData(item, element);
                }
                AddProperty(parent, xmlData.Name.ToString(), item);
            }
        }
        else
        {
            AddProperty(parent, xmlData.Name.ToString(), xmlData.Value.Trim());
        }
    }

    private static void AddProperty(dynamic parent, string name, object value)
    {
        if (parent is List<dynamic>)
        {
            (parent as List<dynamic>).Add(value);
        }
        else
        {
            (parent as IDictionary<string, object>)[name] = value;
        }
    }

    private bool ElementIsCollection(XElement element) => element.Elements(element.Elements().First().Name.LocalName).Count() > 1;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.