Covariance & contravariance — when subtype substitution is safe for types
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
CatwhenAnimalis expected is fine — it IS an Animal. - Parameter types: A subclass method may accept a MORE general type (contravariant). Accepting
Animalwhen onlyCatis 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:
class AnimalFactory { public function create(): Animal {} }
class CatFactory extends AnimalFactory { public function create(): Cat {} } // OK — Cat IS-AN AnimalContravariant parameter types in PHP:
class CatHandler { public function handle(Cat $cat): void {} }
class AnimalHandler extends CatHandler { public function handle(Animal $animal): void {} } // OK — handles moreInvariance: 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
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