Let's assume I have two classes that look like this (the first block of code and the general problem are related to C#):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
These classes cannot be changed in any way (they are part of a 3rd party assembly). Therefore, I cannot make them implement the same interface, or inherit the same class that would then contain IntProperty.
I want to apply some logic on the IntProperty
property of both classes, and in C++ I could use a template class to do that quite easily:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
And then I could do something like this:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
That way I could create a single generic factory class that would work for both ClassA and ClassB.
However, in C#, I have to write two classes with two different where
clauses even though the code for the logic is exactly the same:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
I know that if I want to have different classes in the where
clause, they need to be related, i.e. to inherit the same class, if I want to apply the same code to them in the sense that I described above. It is just that it is very annoying to have two completely identical methods. I also do not want to use reflection because of the performance issues.
Can somebody suggest some approach where this can be written in a more elegant fashion?
Best Answer
Add a proxy interface (sometimes called an adapter, occasionally with subtle differences), implement
LogicToBeApplied
in terms of the proxy, then add a way to construct an instance of this proxy from two lambdas: one for the property get and one for the set.Now, whenever you need to pass in an IProxy but have an instance of the third party classes, you can just pass in some lambdas:
Additionally, you can write simple helpers to construct LamdaProxy instances from instances of A or B. They can even be extension methods to give you a "fluent" style:
And now construction of proxies looks like this:
As for your factory, I'd see if you can refactor it into a "main" factory method that accepts an IProxy and performs all logic on it and other methods that just pass in
new A().Proxied()
ornew B().Proxied()
:There's no way to do the equivalent of your C++ code in C# because C++ templates rely on structural typing. As long as two classes have the same method name and signature, in C++ you can call that method generically on both of them. C# has nominal typing - the name of a class or interface is part of its type. Therefore, the classes
A
andB
cannot be treated the same in any capacity unless an explicit "is a" relationship is defined through either inheritance or interface implementation.If the boilerplate of implementing these methods per class is too much, you can write a function that takes an object and reflectively builds a
LambdaProxy
by looking for a specific property name:This fails abysmally when given objects of incorrect type; reflection inherently introduces the possibility of failures the C# type system cannot prevent. Luckily you can avoid reflection until the maintenance burden of the helpers becomes too great because you're not required to modify the IProxy interface or the LambdaProxy implementation to add the reflective sugar.
Part of the reason this works is that
LambdaProxy
is "maximally generic"; it can adapt any value that implements the "spirit" of the IProxy contract because the implementation of LambdaProxy is completely defined by the given getter and setter functions. It even works if the classes have different names for the property, or different types that are sensibly and safely representable asint
s, or if there's some way to map the concept thatProperty
is supposed to represent to any other features of the class. The indirection provided by the functions gives you maximal flexibility.