Crossing the Generics Divide
Generics are great, until they aren't, and when they aren't is when you don't know the type at compile-time but at runtime. This isn't necessarily a bad thing, and isn't necessarily a design problem. Remember, void Main
is not generic, so at some point, your program needs to cross the generic divide. Sometimes, this is explicit (you instantiate an object of a closed generic type) or implicit (you use a DI container to inject a closed generic type). Jeremy Miller blogged about this as well, and I've seen this scenario come up on many occasions.
The strategies for calling generic code from non-generic code depend a little bit on the nature of the generic code in question, and depends on how "dynamic" you want the calling to be.
In a recent project, in the insurance domain, we had a customer applying for insurance, and they could apply for multiple policies. Each policy had a bit of common information, plus information specific to that policy. Some application processing was the same, some was different. We modeled our domain something like:
All is well and good until you need to do something specific with one of your Policy
classes.
The simplest solution is to "hardcode" it, using something like pattern matching to do something specific with each derived type. Let's say we want to validate each policy in the application. We can loop through and pattern match on the type:
bool isValid = false;
foreach (var policy in application.Policies)
{
isValid = isValid && policy switch
{
HomePolicy home => Validate(home),
LifePolicy life => Validate(life),
AutoPolicy auto => Validate(auto),
};
}
Each Validate
method is simply an overload with a different typed parameter:
private static bool Validate(AutoPolicy auto)
{
}
private static bool Validate(HomePolicy home)
{
}
private static bool Validate(LifePolicy life)
{
}
This method works OK as long as the behavior in those methods is relatively simple and you don't have many conflicting dependencies.
Unfortunately, once you try to extract these methods into classes, perhaps each with their own dependencies, is when things get hairy.
Introducing the generic type
When we extract those generic methods into generic classes, we wind up creating some common interface:
public interface IPolicyValidator<TPolicy>
where TPolicy : IPolicy
{
bool Validate(TPolicy policy);
}
Then we'll have derived types for our different policies:
public class LifePolicyValidator : IPolicyValidator<LifePolicy>
{
public bool Validate(LifePolicy policy)
{
// Validate LifePolicy somehow
Now our logic for individual derived types is neatly encapsulated behind these classes, but we're still able to use the derived type because the incoming parameter has closed to the generic parameter, giving us LifePolicy
instead of IPolicy
. Everything's great right? Not quite! Let's go back to the calling location:
bool isValid = false;
foreach (var policy in application.Policies)
{
var policyValidator = container.GetService<IPolicyValidator<??>>();
isValid = policyValidator.Validate(policy);
}
Instead of instantiating our derived validators, we use a container because our validators might have dependencies they use and we don't want to change our application validation code just because a single policy validator needs to do more stuff. Our problem here is we need to "know" the service type at compile time in order to pass that value through to the IPolicyValidator<TPolicy>
open generic type.
We could go back to pattern matching:
bool isValid = false;
foreach (var policy in application.Policies)
{
switch (policy)
{
case AutoPolicy auto:
var autoPolicyValidator = container.GetService<IPolicyValidator<AutoPolicy>>();
isValid = isValid && autoPolicyValidator.Validate(auto);
break;
case HomePolicy home:
var homePolicyValidator = container.GetService<IPolicyValidator<HomePolicy>>();
isValid = isValid && homePolicyValidator.Validate(home);
break;
case LifePolicy life:
var lifePolicyValidator = container.GetService<IPolicyValidator<LifePolicy>>();
isValid = isValid && lifePolicyValidator.Validate(life);
break;
}
}
Yeesh, that's not pretty. It's all compile-safe, but lots of duplication. Not really an improvement!
What we'd like to do is allow our non-generic code call our generic code, but in a compile-safe way.
There's two major ways of doing this in OO, using inheritance, or composition. Let's look at the inheritance way first.
Wrapping generics using inheritance
One way we can get around this is creating a base type that isn't generic, and that will be the signature our calling class calls:
public interface IPolicyValidator
{
bool Validate(IPolicy policy);
}
That's something our application code can work with. Next, we need to bridge the gap between this non-generic type, and our generic ones. For that, we'll create a type that implements both interfaces - the generic, and non-generic one:
public abstract class PolicyValidator<TPolicy> :
IPolicyValidator,
IPolicyValidator<TPolicy> where TPolicy : IPolicy
{
public bool Validate(IPolicy policy)
=> Validate((TPolicy) policy);
public abstract bool Validate(TPolicy policy);
}
Now our trick here is the non-generic implementation delegates to the generic version, but casting the parameter to the correct generic type TPolicy
. The generic method is now abstract, and our policy validator implementations now inherit this class:
public class LifePolicyValidator : PolicyValidator<LifePolicy>
{
public override bool Validate(LifePolicy policy)
{
We now override
that abstract class instead of implementing it directly. Back in our calling code, we need to now work with this non-generic type. However, we need to get the correct implementation from our container. For that, we can inspect the runtime type to ask for the correct service type from the container:
bool isValid = false;
foreach (var policy in application.Policies)
{
var policyType = policy.GetType();
var validatorType = typeof(IPolicyValidator<>).MakeGenericType(policyType);
var policyValidator = (IPolicyValidator) container.GetService(validatorType);
isValid = isValid && policyValidator.Validate(policy);
}
We ask the container for the correct validator type IPolicyValidator<Whatever>
where we fill in the generic parameters at runtime. We ask container for this service, casting the IPolicyValidator<Whatever>
result into the non-generic type IPolicyValidator
because we """"know"""" that the service type PolicyValidator<TPolicy>
actually implements both IPolicyValidator
and IPolicyValidator<TPolicy>
.
This works great, but if and only if our LifePolicyValidator
inherits from this bridge type. Not great, but the code is relatively straightforward to understand. With the inheritance, we might be able to extract common logic into the base class as well.
However, forcing an inheritance hierarchy isn't ideal in many scenarios, so let's now look at a composition approach.
Wrapping generics using composition
With composition, we'll still have our common, non-generic interface that our calling code uses:
public interface IPolicyValidator
{
bool Validate(IPolicy policy);
}
We'll still need a class bridging between non-generic and generic, but this time, we'll have our bridge class compose the generic implementation instead of implementing/inheriting it:
public class PolicyValidator<TPolicy> : IPolicyValidator
where TPolicy : IPolicy
{
private readonly IPolicyValidator<TPolicy> _inner;
public PolicyValidator(IPolicyValidator<TPolicy> inner)
=> _inner = inner;
public bool Validate(IPolicy policy)
=> _inner.Validate((TPolicy) policy);
}
Our generic bridge class implements the non-generic interface. Its implementation of the non-generic method forwards our non-generic IPolicy
-based method to a generic one and cast along the way, but this time, our generic IPolicyValidator<TPolicy>
is injected, composed into our bridge class. From our calling class, we'll still need to do a bit of manual closing of types:
bool isValid = false;
foreach (var policy in application.Policies)
{
var policyType = policy.GetType();
var validatorType = typeof(PolicyValidator<>).MakeGenericType(policyType);
var policyValidator = (IPolicyValidator) container.GetService(validatorType);
isValid = isValid && policyValidator.Validate(policy);
}
This time, we ask the container for the closed bridge class type, which will get wired up correctly when we register the open type. The container will inspect the closed type and fill in the right services for any dependencies that close the type. Our cast to IPolicyValidator
is much safer now because we control that bridge type and no longer care how the IPolicyValidator<TPolicy>
implementations are created.
So which should you go with? I still prefer the composition route since that still allows me to use inheritance in the implementations when I want, but only when the behavior/logic in my generic types needs it. Both are great to keep in your back pocket to be able to have your generic cake and your non-generic calling code eat it too.