0

Covariance & contravariance — when subtype substitution is safe for types

Advanced5 min read·eng-12-019
interviewcompare

Concept

Covariance and contravariance define when it is safe to substitute a subtype for a supertype in type positions of functions.

Covariance: A type that varies in the SAME direction as a subtype relationship. If Cat extends Animal, then a function returning Cat can substitute for a function returning Animal — the return type is covariant. "More specific return types are safe."

Contravariance: A type that varies in the OPPOSITE direction. A function that accepts Animal can substitute for a function that accepts Cat — the parameter type is contravariant. "More general parameter types are safe."

Liskov Substitution Principle (LSP): The foundation. If B extends A, then B can be used wherever A is expected WITHOUT breaking the program. For this to hold:

  • Return types: A subclass method may return a MORE specific type (covariant). Returning Cat when Animal is expected is fine — it IS an Animal.
  • Parameter types: A subclass method may accept a MORE general type (contravariant). Accepting Animal when only Cat is expected means it'll handle any cat — plus more. No LSP violation.

PHP 7.4 support: PHP added covariant return types in 7.4 and contravariant parameter types in 7.4.

Covariant return types in PHP:

php
class AnimalFactory { public function create(): Animal {} }
class CatFactory extends AnimalFactory { public function create(): Cat {} } // OK — Cat IS-AN Animal

Contravariant parameter types in PHP:

php
class CatHandler { public function handle(Cat $cat): void {} }
class AnimalHandler extends CatHandler { public function handle(Animal $animal): void {} } // OK — handles more

Invariance: When a type must be exactly the same as in the parent (no covariance or contravariance). PHP generics (none yet) would face this issue.

Code Example

php
<?php
class Animal {}
class Cat extends Animal {}
class Kitten extends Cat {}

// ============================================================
// COVARIANT return types — more specific is safe
// ============================================================
interface AnimalRepository
{
    public function find(int $id): Animal; // returns Animal
}

class CatRepository implements AnimalRepository
{
    public function find(int $id): Cat    // returns Cat (more specific) — COVARIANT, OK!
    {
        return Cat::findOrFail($id);
    }
}

// Safe? CatRepository::find() always returns a Cat.
// Cat IS-AN Animal. Code expecting Animal gets a Cat — no issue.
// $repo = new CatRepository();
// $animal = $repo->find(1); // caller sees Animal, gets Cat — LSP satisfied

// ============================================================
// CONTRAVARIANT parameter types — more general is safe
// ============================================================
interface CatFeeder
{
    public function feed(Cat $cat): void;
}

class AnimalFeeder implements CatFeeder
{
    public function feed(Animal $animal): void // accepts Animal (more general) — CONTRAVARIANT, OK!
    {
        echo "Feeding " . get_class($animal) . "\n";
    }
}

// Safe? AnimalFeeder::feed() accepts any Animal, including Cat.
// Calling AnimalFeeder::feed(new Cat()) — it handles cats just fine.
// Caller sends Cat, AnimalFeeder accepts Animal (which Cat is) — LSP satisfied

// ============================================================
// VIOLATION — what breaks LSP
// ============================================================
interface Shape
{
    public function area(): float;
}

class Rectangle implements Shape
{
    public function __construct(protected float $width, protected float $height) {}
    public function area(): float { return $this->width * $this->height; }
    public function setWidth(float $w): void  { $this->width = $w; }
    public function setHeight(float $h): void { $this->height = $h; }
}

class Square extends Rectangle
{
    public function setWidth(float $w): void  { $this->width = $this->height = $w; } // breaks LSP!
    public function setHeight(float $h): void { $this->width = $this->height = $h; } // breaks LSP!
}
// Code that does: $rect->setWidth(4); $rect->setHeight(5); expects area = 20
// But with a Square: area = 25 (height was also set to 5)
// Square is NOT a substitutable Rectangle — LSP violation!
// Fix: don't make Square extend Rectangle — use composition or separate hierarchy