Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tiffany-taylor
Last active December 15, 2019 20:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tiffany-taylor/2656a8011794e367530c33c0a689e0d0 to your computer and use it in GitHub Desktop.
Save tiffany-taylor/2656a8011794e367530c33c0a689e0d0 to your computer and use it in GitHub Desktop.

Covariance and Contravariance

In PHP 7.4, covariance and contravariance were introduced. While these concepts may sound confusing, in practice they're rather simple, and extremely useful to object-oriented programming.

Covariance

Covariance allows a child's method to return a more specific type than the return type of its parent's method. This is better illustrated with an example.

We'll start with a simple abstract parent class, Animal, which is extended by children classes, Cat, and Dog.

abstract class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    abstract public function speak();
}

class Dog extends Animal
{
    public function speak()
    {
        echo $this->name . " barks";
    }
}

class Cat extends Animal 
{
    public function speak()
    {
        echo $this->name . " meows";
    }
}

You'll noticed there aren't any methods which return values in this example. We will build upon these classes with a few factories which return a new object of class type Animal, Cat, or Dog. This is where we can see covariance in action!

interface AnimalShelter
{
    public function adopt(string $name): Animal;
}

class CatShelter implements AnimalShelter
{
    public function adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
    {
        return new Cat($name);
    }
}

class DogShelter implements AnimalShelter
{
    public function adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
    {
        return new Dog($name);
    }
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();

Outputs

Ricky meows
Mavrick barks

Contravariance

Contravariance, on the other hand, allows a parameter type to be less specific in a child method, than that of its parent. Continuing with our previous example with the classes Animal, Cat, and Dog, we're adding a class called Food and AnimalFood, and adding a method eat(AnimalFood $food) to our Animal abstract class.

class Food {}

class AnimalFood extends Food {}

abstract class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function eat(AnimalFood $food)
    {
        echo $this->name . " nom noms " . get_class($food);
    }
}

Now, cats typically are picky eaters. They will eat a specific type of food, and that's it. Dogs, on the other hand, will eat just about anything. Thus, we're going to override the eat method in the Dog class to allow any Food type object. The Cat class remains unchanged.

class Dog extends Animal
{
    public function eat(Food $food) {
        echo $this->name . " nom noms " . get_class($food);
    }
}

With that change, we can see how contravariance works.

$kitty = (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);

Outputs

Ricky nom noms AnimalFood
Mavrick nom noms Food

But what happens if $kitty tries to eat the $banana?

$kitty->eat($banana);

Outputs

Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment