I've been reading up on covariance and contravariance - Wikipedia talks about the following:
Suppose you have a class representing a person. A person can see the doctor, so this class might have a method virtual void Person::see(Doctor d). Now suppose you want to make a subclass of the Person class, Child. That is, a Child is a Person. One might then like to make a subclass of Doctor, Pediatrician. If children only visit pediatricians, we would like to enforce that in the type system. However, a naive implementation fails: because a Child is a Person, Child::see(d) must take any Doctor, not just a Pediatrician.
Here is a "naive implementation":
public interface IDoctor
{
}
public interface IPerson
{
void VisitDoctor(IDoctor doctor);
}
public class Adult : IPerson
{
public void VisitDoctor(IDoctor doctor)
{
Console.WriteLine("Adult saw doctor of type: {0}", doctor.GetType().Name);
}
}
public class Child : IPerson
{
public void VisitDoctor(IDoctor doctor)
{
Console.WriteLine("Child saw doctor of type: {0}", doctor.GetType().Name);
}
}
public class AdultDoctor : IDoctor
{
}
public class ChildDoctor : IDoctor
{
}
These tests:
[Test]
public void AdultSeesDoctor()
{
var adult = new Adult();
adult.VisitDoctor(new AdultDoctor());
adult.VisitDoctor(new ChildDoctor()); // <-- Would like this to fail
}
[Test]
public void ChildSeesDoctor()
{
var child = new Child();
child.VisitDoctor(new AdultDoctor()); // <-- Would like this to fail
child.VisitDoctor(new ChildDoctor());
}
Output:
Adult saw doctor of type: AdultDoctor
Adult saw doctor of type: ChildDoctor
Child saw doctor of type: AdultDoctor
Child saw doctor of type: ChildDoctor
Now, I can implement the following, which throws a runtime error if an adult tries to visit a child doctor, or if a child tries to visit an adult doctor (throws a System.InvalidCastException
):
public interface IVisitDoctors<T> where T : IDoctor
{
void VisitDoctor(T doctor);
}
public class Child : IPerson
{
private readonly ChildDoctorVisitor _cdv = new ChildDoctorVisitor();
public void VisitDoctor(IDoctor doctor)
{
_cdv.VisitDoctor((ChildDoctor)doctor);
}
}
public class Adult : IPerson
{
private readonly AdultDoctorVisitor _adv = new AdultDoctorVisitor();
public void VisitDoctor(IDoctor doctor)
{
_adv.VisitDoctor((AdultDoctor)doctor);
}
}
Could you force classes of Adult
to only visit doctors of type AdultDoctor
, such that a compile-time error is thrown if a doctor of type ChildDoctor
is visited (and vice versa for classes of Child
)?
You don't need co or contra-variance for this:
public interface IDoctor<TPatient> where T : IPerson<TPatient>
{
}
public interface IPerson<T> where T : IPerson<T>
{
void VisitDoctor(IDoctor<T> doctor);
}
public class Adult : IPerson<Adult>
{
void VisitDoctor(IDoctor<Adult> doctor) { }
}
public class AdultDoctor : IDoctor<Adult>
{
}
Now the following will fail to compile:
Adult a = new Adult();
a.VisitDoctor(new ChildDoctor());
while this will:
Adult a = new Adult();
a.VisitDoctor(new AdultDoctor());
This is called the curiously recurring template pattern. In this case, it is used to get the concrete implementor type (Adult
) through the interface type IPerson
. This means that the type of doctor an IPerson
can visit can be restricted to the same type as the implementor.
You can also see it in the Java Enum class. The compareTo
method allows you to compare enums, but the recurring template is needed to ensure that you can only compare enums of the same type.
It is quite ugly however, so you may want to consider changing your design to something like:
public interface IDoctor<TPatient>
{
void SeePatient(TPatient patient);
}
public interface IAppointments<T>
{
void MakeAppointment(T patient, IDoctor<T> doctor);
}
So you can remove the need for IPerson
to have a type parameter.