Introduction
The principles of object oriented software development identify the encapsulation of computing logic that is unique to a problem space and is the sole authority of that computation. The objective is the normalization of computing logic into discrete computing components that can be orchestrated into complex systems by using easier to manage and understand units. The interface to any object consists of the exposed properties and methods that allow other objects to interact with it. Unfortunately, many times, standard structure properties such as dictionaries and lists, are exposed creating a temptation to manipulate the data in these properties, which create a major violation of control. In fact, you will recognize “IDictionary
” was written with the exposed read only collections “Keys
” and “Values
” instead of an actual list. One could only imagine the complications if the IDictionary
exposes the real list in “Keys
” and then the caller started adding to the list. Although one can argue that the list of keys does not really exist in the implementation of IDictionary
, it is easy to see where this can be an issue.
Approach
Read only properties of .NET objects are a fundamental principle of any quality software development, which allows an object to maintain control of the data it is tasked to manage. Unfortunately, it is not uncommon to have a complex property that is a dictionary or list, which exposes the data of the object to changes outside the control of the object. The .NET Framework 4.5 finally provided us “IReadOnlyDictionary<TKey, TValue>
” and “IReadOnlyList<TKey, TValue>
” to rectify this. Now all objects that expose these structures should use them instead as follows:
public class ExampleClass
{
public class ExampleData
{
public int ID { get; private set; }
public string Value { get; private set; }
public ExampleData(int id, string value)
{
ID = id;
Value = value;
}
}
private Dictionary<string, ExampleData> _keyData;
public IReadOnlyDictionary<string, ExampleData> KeyData { get { return _keyData; } }
private List<ExampleData> _listData;
public IReadOnlyList<ExampleData> ListData { get { return _listData; } }
public ExampleClass()
{
_keyData = new Dictionary<string, ExampleData>();
_listData = new List<ExampleData>();
}
}
Public Class ExampleClass
Public Class ExampleData
Public ReadOnly Property ID As Integer
Public ReadOnly Property Value As String
Public Sub New(ByVal id As Integer, ByVal value As String)
Me.ID = id
Me.Value = value
End Sub
End Class
Private _keyData As Dictionary(Of String, ExampleData)
Public ReadOnly Property KeyData As IReadOnlyDictionary(Of String, ExampleData)
Get
Return _keyData
End Get
End Property
Private _listData As List(Of ExampleData)
Public ReadOnly Property ListData As IReadOnlyList(Of ExampleData)
Get
Return _listData
End Get
End Property
Public Sub New()
_keyData = New Dictionary(Of String, ExampleData)()
_listData = New List(Of ExampleData)
End Sub
End Class
Using this coding pattern prevents manipulation of these data structures outside the control of the class, while providing direct access to the data within these structures.
Unfortunately, not all objects that implement the “IDictionary<TKey, TValue>
” or “IList<TKey>
” interface also implements the corresponding “IReadOnlyDictionary<TKey, TValue>
” or “IReadOnlyList<TKey>
” interfaces. An example of this was the original implementation of “ConcurrentDictionary<TKey, TValue>
” and properties of various collections such as “IDictionary<TKey, TValue>.Values
”. To achieve this, you need a wrapper object that limits the interface of these structures to the read-only interfaces such as the following:
public class ReadOnlyDictionaryWrapper<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
private IDictionary<TKey, TValue> _dictionary;
public ReadOnlyDictionaryWrapper(IDictionary<TKey, TValue> dictionary)
{
_dictionary = dictionary;
}
public TValue this[TKey key] { get { return _dictionary[key]; } }
public int Count { get { return _dictionary.Count; } }
public IEnumerable<TKey> Keys { get { return _dictionary.Keys; } }
public IEnumerable<TValue> Values { get { return _dictionary.Values; } }
public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { return _dictionary.GetEnumerator(); }
public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); }
IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_dictionary).GetEnumerator(); }
}
public class ReadOnlyListWrapper<TValue> : IReadOnlyList<TValue>
{
private IList<TValue> _list;
public ReadOnlyListWrapper(IList<TValue> list)
{
_list = list;
}
public TValue this[int index] { get { return _list[index]; } }
public int Count { get { return _list.Count; } }
public IEnumerator<TValue> GetEnumerator() { return _list.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_list).GetEnumerator(); }
}
Public Class ReadOnlyDictionaryWrapper(Of TKey, TValue)
Implements IReadOnlyDictionary(Of TKey, TValue)
Private _dictionary As IDictionary(Of TKey, TValue)
Public Sub New(dictionary As IDictionary(Of TKey, TValue))
Me._dictionary = dictionary
End Sub
Public ReadOnly Property Count As Integer _
Implements IReadOnlyCollection(Of KeyValuePair(Of TKey, TValue)).Count
Get
Return Me._dictionary.Count
End Get
End Property
Default Public ReadOnly Property Item(key As TKey) As TValue _
Implements IReadOnlyDictionary(Of TKey, TValue).Item
Get
Return Me._dictionary(key)
End Get
End Property
Public ReadOnly Property Keys As IEnumerable(Of TKey) _
Implements IReadOnlyDictionary(Of TKey, TValue).Keys
Get
Return Me._dictionary.Keys
End Get
End Property
Public ReadOnly Property Values As IEnumerable(Of TValue) _
Implements IReadOnlyDictionary(Of TKey, TValue).Values
Get
Return Me._dictionary.Values
End Get
End Property
Public Function ContainsKey(key As TKey) As Boolean _
Implements IReadOnlyDictionary(Of TKey, TValue).ContainsKey
Return Me._dictionary.ContainsKey(key)
End Function
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of TKey, TValue)) _
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue)).GetEnumerator
Return Me._dictionary.GetEnumerator()
End Function
Public Function TryGetValue(key As TKey, ByRef value As TValue) As Boolean _
Implements IReadOnlyDictionary(Of TKey, TValue).TryGetValue
Return Me._dictionary.TryGetValue(key, value)
End Function
Private Function IEnumerable_GetEnumerator() As IEnumerator _
Implements IEnumerable.GetEnumerator
Return CType(Me._dictionary, IEnumerable).GetEnumerator()
End Function
End Class
Public Class ReadOnlyListWrapper(Of TValue)
Implements IReadOnlyList(Of TValue)
Private _list As IList(Of TValue)
Public Sub New(list As IList(Of TValue))
Me._list = list
End Sub
Public ReadOnly Property Count As Integer _
Implements IReadOnlyCollection(Of TValue).Count
Get
Return Me._list.Count
End Get
End Property
Default Public ReadOnly Property Item(index As Integer) As TValue _
Implements IReadOnlyList(Of TValue).Item
Get
Return Me._list(index)
End Get
End Property
Public Function GetEnumerator() As IEnumerator(Of TValue) _
Implements IEnumerable(Of TValue).GetEnumerator
Return Me._list.GetEnumerator()
End Function
Private Function IEnumerable_GetEnumerator() As IEnumerator _
Implements IEnumerable.GetEnumerator
Return CType(Me._list, IEnumerable).GetEnumerator()
End Function
End Class
This allows any “IDictionary<TKey, TValue>
” or “IList<TKey>
” object to be published as a read only object as in the following example:
public class ExampleClass
{
public class ExampleData
{
public int ID { get; private set; }
public string Value { get; private set; }
public ExampleData(int id, string value)
{
ID = id;
Value = value;
}
}
private Dictionary<string, ExampleData> _keyData;
public IReadOnlyDictionary<string, ExampleData> KeyData { get; private set; }
private List<ExampleData> _listData;
public IReadOnlyList<ExampleData> ListData { get; private set; }
public ExampleClass()
{
_keyData = new Dictionary<string, ExampleData>();
KeyData = new ReadOnlyDictionaryWrapper<string, ExampleData>(_keyData);
_listData = new List<ExampleData>();
ListData = new ReadOnlyListWrapper<ExampleData>(_listData);
}
}
Public Class ExampleClass
Public Class ExampleData
Public ReadOnly Property ID As Integer
Public ReadOnly Property Value As String
Public Sub New(ByVal id As Integer, ByVal value As String)
Me.ID = id
Me.Value = value
End Sub
End Class
Private _keyData As Dictionary(Of String, ExampleData)
Private _keyReadOnlyData As ReadOnlyDictionaryWrapper(Of String, ExampleData)
Public ReadOnly Property KeyData As IReadOnlyDictionary(Of String, ExampleData)
Get
Return _keyReadOnlyData
End Get
End Property
Private _listData As List(Of ExampleData)
Private _listReadOnlyData As ReadOnlyListWrapper(Of ExampleData)
Public ReadOnly Property ListData As IReadOnlyList(Of ExampleData)
Get
Return _listData
End Get
End Property
Public Sub New()
_keyData = New Dictionary(Of String, ExampleData)()
_keyReadOnlyData = New ReadOnlyDictionaryWrapper(Of String, ExampleData)(_keyData)
_listData = New List(Of ExampleData)
_listReadOnlyData = New ReadOnlyListWrapper(Of ExampleData)(_listData)
End Sub
End Class
The .NET Framework does implement wrapper classes in the namespace “System.Collections.ObjectModel
” called “ReadOnlyDictionary
” and “ReadOnlyCollection
”, but unfortunately these objects are designed to make a copy of the data structure properties, such as “Keys
” and “Values
”, when created. The advantage of the wrapper classes “ReadOnlyDictionaryWrapper
” and “ReadOnlyListWrapper
” above is the implementation is very lightweight, continues to track changes in the underlying structure and the users of the object can maintain references to the object.
The disadvantage of these objects is the same as the original object, the underlying dictionary or list structure may change while iterating through the structure. This can be easily resolved by creating copy objects that derived from these wrapper objects as such:
public class ReadOnlyDictionaryCopy<TKey, TValue> : ReadOnlyDictionaryWrapper<TKey, TValue>
{
public ReadOnlyDictionaryCopy(IEnumerable<KeyValuePair<TKey, TValue>> dictionaryList)
: base(_copy(dictionaryList))
{ }
private static IDictionary<TKey, TValue> _copy(IEnumerable<KeyValuePair<TKey, TValue>> dictionaryList)
{
var result = new Dictionary<TKey, TValue>();
foreach (KeyValuePair<TKey, TValue> kv in dictionaryList)
result.Add(kv.Key, kv.Value);
return result;
}
}
public class ReadOnlyListCopy<TValue> : ReadOnlyListWrapper<TValue>
{
public ReadOnlyListCopy(IEnumerable<TValue> list)
: base(new List<TValue>(list))
{ }
}
Public Class ReadOnlyDictionaryCopy(Of TKey, TValue)
Inherits ReadOnlyDictionaryWrapper(Of TKey, TValue)
Public Sub New(ByVal dictionaryList As IEnumerable(Of KeyValuePair(Of TKey, TValue)))
MyBase.New(_copy(dictionaryList))
End Sub
Private Shared Function _copy(ByVal dictionaryList As IEnumerable(Of KeyValuePair(Of TKey, TValue))) _
As IDictionary(Of TKey, TValue)
Dim result As New Dictionary(Of TKey, TValue)()
For Each kv As KeyValuePair(Of TKey, TValue) In dictionaryList
result.Add(kv.Key, kv.Value)
Next
Return result
End Function
End Class
Public Class ReadOnlyListCopy(Of TValue)
Inherits ReadOnlyListWrapper(Of TValue)
Public Sub New(ByVal list As IEnumberable(Of TValue))
MyBase.New(New List(Of TValue)(list))
End Sub
End Class
A caller can then easily make a read-only copy of any dictionary or list by using the example code below:
var myKeyData = new ReadOnlyDictionaryCopy<string, ExampleData>(myInstance.KeyData);
var myListData = new ReadOnlyListCopy<ExampleData>(myInstance.ListData);
Dim myKeyData As New ReadOnlyDictionaryCopy(Of String, ExampleData)(myInstance.KeyData)
Dim myListData As New ReadOnlyListCopy(Of ExampleData)(myInstance.ListData)
Conclusion
Maintaining control of the data within an object should be paramount in any well-structured object oriented, since objects are intended to provide services and control logic.