1

I would like to have type-safe access to items in a collection with polymorphic items at compile-time. For this, I set up the code below as a proof of concept. Passing the DataItemProps instances to the GetValue, SetValue methods of the Data class, it is possible to resolve the type of an item during compile-time, allowing type-safe setting and getting of values.

I would like to extend this pattern to also support aliases. For this, I could add Aliases to the DataItemProps, and modify the Data class to also take these aliases into account. But I wonder if that should be the concern of the Data class.

The fact that for every change in the DataItemProps class, I have some changes to do in the Data class, does that mean it is too tightly coupled?

One other solution I thought of was to let the DataItemProps class itself be responsible for getting/setting a value in the Data class, passing the Data instance as a parameter in the method (e.g: MyDataItemProps.SetValue(data, 123);), but this didn't feel right as well.

My questions:

  • I mainly wonder what the right approach would be. Letting Data be responsible for getting/setting using DataItemProps, or the other way around? In the case of the latter, I think 'Props' is a bad name, what would be a good name for this class?
  • Should the Default and even 'Aliases' be part of the DataItemProps class?
class Program
{
    static void Main(string[] args)
    {
        var data = new Data();
        data.SetValue(KnownKeys.FooInt, 12);
        var res = data.GetValue(KnownKeys.FooInt);
    }
}

public class KnownKeys
{
    public static readonly DataItemProps<int> FooInt = new DataItemProps<int>("FooInt", 123);
    public static readonly DataItemProps<string> FooStr = new DataItemProps<string>("FooStr", "I'm a string");
}

public interface IDataItem
{
    string Key { get; set; }
    object Value { get; set; }
}

public interface IDataItem<T> : IDataItem
{
    new T Value { get; set; }
}

public class DataItem<T> : IDataItem<T>
{
    public string Key { get; set; }
    public T Value { get; set; }
    object IDataItem.Value { get => Value; set => Value = (T) value; }
}

public class Data : Collection<IDataItem>
{
    public T GetValue<T>(DataItemProps<T> key)
    {
        return GetDataItem(key).Value;
    }

    public T GetValueOrDefault<T>(DataItemProps<T> key)
    {
        var di = GetDataItemOrNull(key);
        if (di is null)
            return key.Default;
        else
            return di.Value;
    }

    public IDataItem<T> GetDataItemOrNull<T>(DataItemProps<T> key)
    {
        return (IDataItem<T>) Items.FirstOrDefault(x => x.Key == key.Key);
    }

    public IDataItem<T> GetDataItem<T>(DataItemProps<T> key)
    {
        return GetDataItemOrNull(key) ?? throw new KeyNotFoundException();
    }

    public void SetValue<T>(DataItemProps<T> key, T value)
    {
        var di = GetDataItemOrNull(key);
        if (di is null)
        {
            di = new DataItem<T>() { Key = key.Key, Value = value };
            Add(di);
        }           
    }

    public void SetDefault<T>(DataItemProps<T> key)
    {
        SetValue(key, key.Default);
    }
}

public class DataItemProps<T> 
{
    public DataItemProps(string name, T @default)
    {
        Key = name;
        Default = @default;
    }

    /// <summary>
    /// Key to look for in the dictionary.
    /// </summary>
    public string Key { get; set; }

    /// <summary>
    /// The default of the key.
    /// </summary>
    public T Default { get; set; }
}

This is the alternate approach Also note the ExtendedDataItemProps at the bottom containing Aliases

    class Program
    {
        static void Main(string[] args)
        {
            var data = new Data();
            KnownKeys.FooInt.SetValue(data, 12);
            var res = KnownKeys.FooInt.GetValue(data);
        }
    }

    public class KnownKeys
    {
        public static readonly DataItemProps<int> FooInt = new DataItemProps<int>("FooInt", 123);
        public static readonly DataItemProps<string> FooStr = new DataItemProps<string>("FooStr", "I'm a string");
    }

    public interface IDataItem
    {
        string Key { get; set; }
        object Value { get; set; }
    }

    public interface IDataItem<T> : IDataItem
    {
        new T Value { get; set; }
    }

    public class DataItem<T> : IDataItem<T>
    {
        public string Key { get; set; }
        public T Value { get; set; }
        object IDataItem.Value { get => Value; set => Value = (T) value; }
    }

    public class Data : Collection<IDataItem>
    {

    }

    public class DataItemProps<T> 
    {
        public DataItemProps(string name, T @default)
        {
            Key = name;
            Default = @default;
        }

        /// <summary>
        /// Key to look for in the dictionary.
        /// </summary>
        public string Key { get; set; }

        /// <summary>
        /// The default of the key.
        /// </summary>
        public T Default { get; set; }


        public T GetValue(Data data)
        {
            return GetDataItem(data).Value;
        }

        public T GetValueOrDefault(Data data)
        {
            var di = GetDataItemOrNull(data);
            if (di is null)
                return Default;
            else
                return di.Value;
        }

        public virtual IDataItem<T> GetDataItemOrNull(Data data)
        {
            return (IDataItem<T>)data.FirstOrDefault(x => x.Key == Key);
        }

        public IDataItem<T> GetDataItem(Data data)
        {
            return GetDataItemOrNull(data) ?? throw new KeyNotFoundException();
        }

        public void SetValue(Data data, T value)
        {
            var di = GetDataItemOrNull(data);
            if (di is null)
            {
                di = new DataItem<T>() { Key = Key, Value = value };
                data.Add(di);
            }
        }

        public void SetDefault(Data data, DataItemProps<T> key)
        {
            SetValue(data, Default);
        }
    }

    public class ExtendedDataItemProps<T> : DataItemProps<T>
    {
        public ExtendedDataItemProps(string name, T @default, params string[] aliases) : base(name, @default)
        {
            Aliases = aliases;
        }

        public string[] Aliases { get; }

        public override IDataItem<T> GetDataItemOrNull(Data data)
        {
            return base.GetDataItemOrNull(data) ?? (IDataItem<T>) data.FirstOrDefault(x => Aliases.Contains(x.Key));
        }
    }


2
  • Was this design inspired by WPF dependency properties? The basic idea looks to be about the same - they do a thing similar to your first approach, but have different motivations. Could you expand a bit on what these aliases represent in the overall application, and how they are going to be used? E.g., if you don't need to convert back and forth between aliases, then you could just create, say, a new DataItemProps<int>("FooInt", 123) if the given alias was from a prefefined set of aliases, and then forget about the input (alias) string altogether, and just work with the DataItemProps object. Jan 11, 2022 at 16:53
  • @FilipMilovanović: The aliases are mainly used to support backward compatibility in the data sets. Older data should still work with older versions of the software but some names of data items have changed over the years. The data item "FooInt" could have been called "Foo-int" before, so to allow newer software to also read this data item I introduced aliases. I also use the aliases for user-interfaces etc. It does indeed look similar to dependency properties! Jan 11, 2022 at 17:02

1 Answer 1

2

I still lack some of the context, but based on what you've described, of the two client APIs you came up with, this one makes more sense to me:

var data = new Data();
data.SetValue(KnownKeys.FooInt, 12);
var res = data.GetValue(KnownKeys.FooInt);

There is a nugget of the right idea, I think, in your second approach - namely to make your DataItemProps more than just a data structure. However, I don't think that this class should have the responsibility of actually pulling the data item out of the data object.

Based on your comments, I'm assuming that the plain string keys are coming from the outside (i.e. your application doesn't have control of what the actual keys are), and that one of the responsibilities of your application is to recognize that a particular key belongs to a known set of aliases for a particular data item.

Your DataItemProps seems like a good place to encapsulate that responsibility. For simplicity, I'm just going to use the convention that the first element of the keyAliases collection is the default key. So, perhaps something like this:

public class DataItemProps<T> 
{
    private List<string> _keyAliases;
    
    public DataItemProps(IEnumerable<string> keyAliases, T defaultValue)
    {
        _keyAliases = keyAliases.ToList();  // make a copy
        DefaultValue = defaultValue;
    }

    // might need this data elsewhere (consider returning a copy)
    public IEnumerable<string> KeyAliases { ... }  
    public string DefaultKey => _keyAliases[0];

    public T DefaultValue { get; set; }

    public bool RecognizesKey(string key) 
    {
        return _keyAliases.Contains(key);
    }
}

Think of it as representing metadata about the keys associated with a data item. I wouldn't make the class much more complicated than this. A sort of a descriptor of how the data item is identified, with some type & default value information, and a limited set of behaviors focused on working with the string keys. (On that note, if your collection of polymorphic data items can be seen as a single data structure, then a "data item" is simply a property, so you could maybe rename DataItemProps to something like PropertyMetadata, PropertyDescriptor, or PropertyInfo - although, there's a class with that name in System.Reflection - or if it's not conceptually a property, then perhaps KeyMetadata, KeyInfo. Just throwing ideas out there, you might come up with a more suitable name.)

Then you can do:

public IDataItem<T> GetDataItemOrNull<T>(DataItemProps<T> dataItemProp)
{
        return (IDataItem<T>) Items.FirstOrDefault(x => dataItemProp.RecognizesKey(x.Key));
}

// and

public void SetValue<T>(DataItemProps<T> dataItemProp, T value)
{
    var di = GetDataItemOrNull(dataItemProp);
    if (di is null)
    {
        di = new DataItem<T>() { Key = dataItemProp.DefaultKey };
        Items.Add(di);
    }

    di.Value = value;   
    
    // NOTE: I'm not checking if the new value is same as the old value, 
    // but you might want to, if you're doing something like firing change events
}

I'm also assuming that the source data can't have two different properties (data items) that have keys that are aliases of each other.

The fact that for every change in the DataItemProps class, I have some changes to do in the Data class, does that mean it is too tightly coupled?

If you feel that the changes ought to be largely independent, then probably yes, but more importantly, it probably means that the implementation is not cohesive enough - you are reaching out to other objects and manipulating their internals in order to accomplish something. That's why they are coupled. To deal with that, try to restructure the other object so that you can pull those aspects out of it and place them into the first one ("things that change together go together") - and then have the objects interact through well-defined interfaces focused around that separation (by "interface", I just mean public members on each class; there may or may not be a C# interface type involved).

Also note that you can't decouple the two for every type of change imaginable, but if you know which kinds of changes are the most likely, then you can decouple against those (basically, you strive to restructure things so that the interfaces & the way the classes interact are relatively stable in the face of these common kinds of changes).

1
  • Thank you for taking your time to make such a constructive answer! I am always trying to decouple as much as possible, but as you say, sometimes coupling cannot be avoided. Based on your answer I understand that putting the methods within DataItemProps is not a bad idea, but it makes things complicated, and basically, the only thing that is needed is something like: recognizesKey. I will proceed to keep the logic within the Data class, and keep the DataItemProps simple. I will probably go with PropertyDescriptor. Jan 11, 2022 at 19:19

Your Answer

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

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