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
Databe responsible for getting/setting usingDataItemProps, 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
Defaultand even 'Aliases' be part of theDataItemPropsclass?
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)); } }
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.