Question

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)?

Was it helpful?

Solution

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.

OTHER TIPS

I understand what you would like to achieve. But if you will keep in play this interface:

public interface IPerson
{
    void VisitDoctor(IDoctor doctor);
}

then any further restriction on implementers will break the Liskov principle. And that will most likely later cause some problems to your code.

What you can do is place that restriction into your interface declaration

public interface IPerson<TDoctor>
    where TDoctor : class, IDoctor...
{
    void VisitDoctor(TDoctor doctor);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top