Aller au contenu

L'héritage et le polymorphisme

🤖 Rédaction assistée par IA.

Définition

L'héritage permet de définir une classe dérivée (ex : Circle) à partir d'une classe de base (ex : Shape) dont elle reprend les attributs et les méthodes. Il traduit une relation « est un » (un cercle est une forme).

Le polymorphisme permet de manipuler des objets de types différents à travers un type commun : lors de l'appel d'une méthode (ex : shape.render()), c'est l'implémentation propre au type réel de l'objet qui est exécutée.

Une méthode abstraite est déclarée dans la classe de base sans y être implémentée : chaque classe dérivée doit en fournir l'implémentation.

Cas d'école

Considérons une fonction qui dessine des formes géométriques :

function render(shape: Shape): string {
    if (shape instanceof Circle) {
        // dessin d'un cercle
    } else if (shape instanceof Rectangle) {
        // dessin d'un rectangle
    } else {
        throw new TypeNotSupportedError(shape);
    }
}

Avec cette approche :

  • L'ajout d'une nouvelle forme (ex : Triangle) impose de modifier toutes les fonctions qui testent le type des formes.
  • L'oubli d'un cas ne sera détecté qu'à l'exécution (TypeNotSupportedError).

Solution

Le principe consiste à déclarer une méthode abstraite render dans la classe de base Shape et à l'implémenter dans chaque classe dérivée :

classDiagram
    class Shape {
        <<abstract>>
        +render()* string
    }
    class Circle {
        -x: double
        -y: double
        -radius: double
        +render() string
    }
    class Rectangle {
        -x: double
        -y: double
        -width: double
        -height: double
        +render() string
    }
    Shape <|-- Circle
    Shape <|-- Rectangle

Pour simplifier, render renvoie ici un fragment SVG :

abstract class Shape {
    abstract render(): string;
}

class Circle extends Shape {
    constructor(private x: number, private y: number, private radius: number) {
        super();
    }

    render(): string {
        return `<circle cx="${this.x}" cy="${this.y}" r="${this.radius}" />`;
    }
}

class Rectangle extends Shape {
    constructor(private x: number, private y: number, private width: number, private height: number) {
        super();
    }

    render(): string {
        return `<rect x="${this.x}" y="${this.y}" width="${this.width}" height="${this.height}" />`;
    }
}

const shapes: Shape[] = [new Circle(5, 5, 2), new Rectangle(0, 0, 4, 3)];
for (const shape of shapes) {
    console.log(shape.render()); // appelle Circle.render() ou Rectangle.render()
}
abstract class Shape {
    public abstract String render();
}

class Circle extends Shape {
    private double x;
    private double y;
    private double radius;

    public Circle(double x, double y, double radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    @Override
    public String render() {
        return String.format("<circle cx=\"%s\" cy=\"%s\" r=\"%s\" />", x, y, radius);
    }
}

class Rectangle extends Shape {
    private double x;
    private double y;
    private double width;
    private double height;

    public Rectangle(double x, double y, double width, double height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    @Override
    public String render() {
        return String.format("<rect x=\"%s\" y=\"%s\" width=\"%s\" height=\"%s\" />", x, y, width, height);
    }
}

List<Shape> shapes = List.of(new Circle(5, 5, 2), new Rectangle(0, 0, 4, 3));
for (Shape shape : shapes) {
    System.out.println(shape.render()); // appelle Circle.render() ou Rectangle.render()
}
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def render(self) -> str: ...


class Circle(Shape):
    def __init__(self, x: float, y: float, radius: float) -> None:
        self.x = x
        self.y = y
        self.radius = radius

    def render(self) -> str:
        return f'<circle cx="{self.x}" cy="{self.y}" r="{self.radius}" />'


class Rectangle(Shape):
    def __init__(self, x: float, y: float, width: float, height: float) -> None:
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def render(self) -> str:
        return f'<rect x="{self.x}" y="{self.y}" width="{self.width}" height="{self.height}" />'


shapes: list[Shape] = [Circle(5, 5, 2), Rectangle(0, 0, 4, 3)]
for shape in shapes:
    print(shape.render())  # appelle Circle.render() ou Rectangle.render()
class Shape {
public:
    virtual ~Shape() = default;
    virtual std::string render() const = 0;
};

class Circle : public Shape {
public:
    Circle(double x, double y, double radius) : x(x), y(y), radius(radius) {}

    std::string render() const override {
        std::ostringstream os;
        os << "<circle cx=\"" << x << "\" cy=\"" << y << "\" r=\"" << radius << "\" />";
        return os.str();
    }

private:
    double x;
    double y;
    double radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double x, double y, double width, double height)
        : x(x), y(y), width(width), height(height) {}

    std::string render() const override {
        std::ostringstream os;
        os << "<rect x=\"" << x << "\" y=\"" << y << "\" width=\"" << width << "\" height=\"" << height << "\" />";
        return os.str();
    }

private:
    double x;
    double y;
    double width;
    double height;
};

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5, 5, 2));
shapes.push_back(std::make_unique<Rectangle>(0, 0, 4, 3));
for (const auto& shape : shapes) {
    std::cout << shape->render() << std::endl; // appelle Circle::render() ou Rectangle::render()
}
abstract class Shape
{
    abstract public function render(): string;
}

class Circle extends Shape
{
    public function __construct(
        private float $x,
        private float $y,
        private float $radius,
    ) {
    }

    public function render(): string
    {
        return sprintf('<circle cx="%s" cy="%s" r="%s" />', $this->x, $this->y, $this->radius);
    }
}

class Rectangle extends Shape
{
    public function __construct(
        private float $x,
        private float $y,
        private float $width,
        private float $height,
    ) {
    }

    public function render(): string
    {
        return sprintf('<rect x="%s" y="%s" width="%s" height="%s" />', $this->x, $this->y, $this->width, $this->height);
    }
}

$shapes = [new Circle(5, 5, 2), new Rectangle(0, 0, 4, 3)];
foreach ($shapes as $shape) {
    echo $shape->render(), "\n"; // appelle Circle::render() ou Rectangle::render()
}

L'ajout d'une forme Triangle se limite alors à ajouter une classe implémentant render : le code qui parcourt les formes reste inchangé. De plus, l'oubli de l'implémentation de render sera signalé dès la compilation avec un typage statique.

Variantes

Liaison dynamique en C++

En C++, le polymorphisme n'est pas le comportement par défaut : une méthode doit être déclarée virtual pour que l'implémentation du type réel soit appelée. Sans virtual, shape.render() appellera Shape::render() même si shape référence un Circle.

Le mot clé override permet de vérifier à la compilation qu'une méthode redéfinit bien une méthode virtuelle.

Polymorphisme sans héritage

Les langages à typage dynamique permettent d'appeler shape.render() sur tout objet disposant d'une méthode render, sans que les classes héritent d'une classe commune (duck typing, voir Les spécificités des langages).

Avec un typage statique, un type commun reste nécessaire mais il pourra prendre la forme d'une interface plutôt que d'une classe de base.

Mises en garde

  • L'héritage doit traduire une relation « est un » : une classe dérivée doit pouvoir être utilisée partout où la classe de base est attendue (principe de substitution de Liskov).
  • L'héritage n'est pas le seul moyen de réutiliser du code : la composition (un objet en utilise un autre) est souvent plus souple (voir par exemple les patrons Strategy et Decorator).
  • Les hiérarchies profondes (nombreux niveaux d'héritage) deviennent difficiles à comprendre et à faire évoluer.

Liens utiles