visit
// This class is representing the simple entity we are going to use
// in our example.
public class Data
{
public int DataId { get; set; }
public string DataDescription { get; set; }
}
// This class is the first kind of the parent Data class.
public class EmployeeData : Data
{
public string EmployeeName { get; set; }
}
// This class is the second kind of the parent Data class.
public class AssetData : Data
{
public int AssetId { get; set; }
public string AssetName { get; set; }
}
// This is the interface we are going to focus on.
// It is an interface for something that would be able
// to read and write data to something that is ready
// to be used this way.
public interface IReaderWriter<TData> where TData : Data
{
void Initialize();
TData Read(int dataId);
void Write(TData data);
}
We notice on this interface the following:
Data
.Initialize
method.Read(int dataId)
method which expects an integer parameter and returns a TData
.Write(TData data)
method which expects a TData
parameter.
// This is the class implementing our IReaderWriter<TData>
// but now we know that it is going to save and retrieve
// data to and from a file. We would not care about the
// implementation so don't give it too much thought.
public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
public void Initialize() { throw new NotImplementedException(); }
public TData Read(int dataId) { throw new NotImplementedException(); }
public void Write(TData data) { throw new NotImplementedException(); }
}
We notice on this class the following:
Now, in your system, you have a module dedicated to managing Employee data, let’s call this module; Employee Module.
Inside the Employee Module, you are sure of the type of data you are dealing with, it is obviously of type EmployeeData
.
var employeeDataFileReaderWriter = new FileReaderWriter<EmployeeData>();
employeeDataFileReaderWriter.Initialize();
employeeDataFileReaderWriter.Write(new EmployeeData
{ DataId = 1, DataDescription = "Some description.", EmployeeName = "Ahmed" });
var ahmed = employeeDataFileReaderWriter.Read(1);
Now, in your system, you have a module dedicated to managing Asset data, let’s call this module; Asset Module.
Inside the Asset Module, you are sure of the type of data you are dealing with, it is obviously of type AssetData
.
var assetDataFileReaderWriter = new FileReaderWriter<AssetData>();
assetDataFileReaderWriter.Initialize();
assetDataFileReaderWriter.Write(new AssetData
{ DataId = 2, DataDescription = "Some description.", AssetId = 5, AssetName = "Asset 5."});
var asset5 = assetDataFileReaderWriter.Read(2);
Now let’s assume that you have a common module that has a Run(IReaderWriter readerWriter)
method. Inside this method, you want to call the Initialize
method of the passed in readerWriter
parameter.
It is clear now that you can’t do it as you don’t have a non-generic definition of the IReaderWriter
interface. In other words, we only have IReaderWriter<TData>
, not IReaderWriter
.
Huh, it is a piece of cake. Let’s use
IReaderWriter<object>
. Every class is a child of Object, right?….. genius.
You need more explanation right, the short answer is that your interface is Invariant; you can’t call a method expecting IReaderWriter<SomeClass>
and pass in an instance of IReaderWriter<AnyOtherClass>
. The only acceptable call would be with passing IReaderWriter<SomeClass>
, nothing else.
Now you understand why we need to define a non-generic IReaderWriter
interface.
public interface IReaderWriter
{
void Initialize();
}
public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
TData Read(int dataId);
void Write(TData data);
}
Now, we can notice the following:
Initialize()
method as this is what we actually need in the common module or even the new ones if they come in the future.Read
and Write
methods without changing anything on the signature.
Finally, it is working. Let’s celebrate and get something to eat and drink, what a trip :)
I don’t want to be a bearer of bad news here, but, I have bad news…
You have new requirements coming in and the common module needs some modifications. The common module now should be able to store and retrieve data in and from a Blob storage. The Blob storage can store any kind of data.
It is clear now that it is not going to work as IReaderWriter
interface doesn’t define the Read
and Write
methods. They are defined in the IReaderWriter<TData>
. However, on the common module and at the moment of calling StoreInBlob
and RetrieveFromBlob
methods, we don’t know the type of data. So, what to do!!!
public interface IReaderWriter
{
void Initialize();
Data Read(int dataId);
void Write(Data data);
}
public class FileReaderWriter : IReaderWriter
{
public void Initialize() { throw new NotImplementedException(); }
public Data Read(int dataId) { throw new NotImplementedException(); }
public void Write(Data data) { throw new NotImplementedException(); }
}
Now you have to cast your Employee object so that you can access its unique members, like EmployeeName
as in the image.
Similarly, you have to cast your Asset object so that you can access its unique members, like AssetName
as in the image.
The keyword for the best solution here is the word new
. Let me break it down for you.
public interface IReaderWriter
{
void Initialize();
Data Read(int dataId);
void Write(Data data);
}
public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
new TData Read(int dataId);
}
We can notice the following:
IReaderWriter
interface now defines are the required methods.Read
and Write
methods, they are now using the parent Data
entity type.IReaderWriter<TData>
interface now extends the IReaderWriter
interface.IReaderWriter<TData>
interface, we need to use the generic type TData
, not the parent Data
.TData Read(int dataId);
and void Write(TData data);
methods to the IReaderWriter<TData>
interface.Data
or the other one returning TData
.new
keyword at the start of the method definition as in the code above.Read
method inherited from the parent and replace it with the one defined after the new
keyword.Write
method?Write
which expects a parameter of type Data
which is the parent of all types that could be passed in to the Write
method.TData
could come.new
keyword with the Write
method, you would get a warning that you are actually not hiding anything from the parent interface. This is logical as the two Write
methods have different input parameter types, so, it is sound and clear to the compiler that they are two different methods.
public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
public void Initialize() { throw new NotImplementedException(); }
public TData Read(int dataId) { throw new NotImplementedException(); }
public void Write(TData data) { throw new NotImplementedException(); }
Data IReaderWriter.Read(int dataId) { return Read(dataId); }
void IReaderWriter.Write(Data data) { Write((TData)data); }
}
We can notice the following:
Data IReaderWriter.Read(int dataId) { return Read(dataId); }
.Data Read(int dataId);
method defined in the parent IReaderWriter
interface.FileReaderWriter<TData>
class is casted, implicitly or explicitly, as the non-generic interface IReaderWriter
, this Read
method implementation would be used.void IReaderWriter.Write(Data data) { Write((TData)data); }
.void Write(Data data);
method defined in the parent IReaderWriter
interface.FileReaderWriter<TData>
class is casted, implicitly or explicitly, as the non-generic interface IReaderWriter
, this Write
method implementation would be used.
This now leads to the following:
This design technique -afraid of even calling it a pattern- is already used in .NET classes you are using daily. Did you notice that in .NET we have IEnumerable
and IEnumerable<T>
?
Could you imagine what life would be like if we didn’t have IEnumerable
:) ?
You can argue that you still can write a method that accepts <T>
and then it would pass it to the IEnumerable<T>
, but my friend, this would keep bubbling up till you eventually would have to choose an entity type. This entity type is not always defined on all layers or levels of code as we proved above.
Also published here.