Aller au contenu

Les méthodes statiques

🤖 Rédaction assistée par IA.

Définition

Une méthode statique (ou méthode de classe) appartient à la classe et non à ses instances :

  • Elle est appelée sans créer d'instance (ex : Coordinate.fromString("3,4")).
  • Elle n'a pas accès à l'instance courante (this / self), mais peut accéder aux attributs statiques et créer des instances de la classe.

Cas d'école

Reprenons la classe Coordinate de la fiche Les constructeurs. Nous souhaitons créer une coordonnée à partir d'une chaîne de caractères (ex : "3,4" ou "3,4,5").

Sans méthode statique, nous serons amenés à :

  • Définir une fonction utilitaire (ex : parseCoordinate(text)) qui n'est pas rattachée au concept de coordonnée.
  • Ou surcharger le constructeur (ex : new Coordinate("3,4")), ce qui n'est pas possible dans tous les langages et rend l'intention moins lisible.

Solution

Le principe consiste à définir une méthode de fabrique statique fromString dans la classe Coordinate :

classDiagram
    class Coordinate {
        +x: double
        +y: double
        +z: double
        +Coordinate(x: double, y: double, z: double = NaN)
        +fromString(text: string)$ Coordinate
    }
class Coordinate {
    constructor(
        readonly x: number,
        readonly y: number,
        readonly z: number = NaN,
    ) {}

    static fromString(text: string): Coordinate {
        const values = text.split(",").map(Number);
        if (values.length < 2 || values.length > 3 || values.some(Number.isNaN)) {
            throw new Error(`coordonnée invalide : "${text}"`);
        }
        return new Coordinate(values[0], values[1], values[2] ?? NaN);
    }
}

const c = Coordinate.fromString("3,4"); // appel sans instance
class Coordinate {
    private final double x;
    private final double y;
    private final double z;

    public Coordinate(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public static Coordinate fromString(String text) {
        String[] parts = text.split(",");
        if (parts.length < 2 || parts.length > 3) {
            throw new IllegalArgumentException("coordonnée invalide : \"" + text + "\"");
        }
        double x = Double.parseDouble(parts[0]);
        double y = Double.parseDouble(parts[1]);
        double z = parts.length == 3 ? Double.parseDouble(parts[2]) : Double.NaN;
        return new Coordinate(x, y, z);
    }
}

Coordinate c = Coordinate.fromString("3,4"); // appel sans instance
import math
from typing import Self


class Coordinate:
    def __init__(self, x: float, y: float, z: float = math.nan) -> None:
        self.x = x
        self.y = y
        self.z = z

    @classmethod
    def from_string(cls, text: str) -> Self:
        values = [float(value) for value in text.split(",")]
        if not 2 <= len(values) <= 3:
            raise ValueError(f'coordonnée invalide : "{text}"')
        return cls(*values)


c = Coordinate.from_string("3,4")  # appel sans instance
class Coordinate {
public:
    Coordinate(double x, double y, double z = std::numeric_limits<double>::quiet_NaN())
        : x(x), y(y), z(z) {}

    static Coordinate fromString(const std::string& text) {
        std::vector<double> values;
        std::istringstream stream(text);
        std::string part;
        while (std::getline(stream, part, ',')) {
            values.push_back(std::stod(part));
        }
        if (values.size() < 2 || values.size() > 3) {
            throw std::invalid_argument("coordonnée invalide : \"" + text + "\"");
        }
        return values.size() == 3 ? Coordinate(values[0], values[1], values[2])
                                  : Coordinate(values[0], values[1]);
    }

    double x;
    double y;
    double z;
};

Coordinate c = Coordinate::fromString("3,4"); // appel sans instance
class Coordinate
{
    public function __construct(
        public readonly float $x,
        public readonly float $y,
        public readonly float $z = NAN,
    ) {
    }

    public static function fromString(string $text): static
    {
        $parts = explode(',', $text);
        if (count($parts) < 2 || count($parts) > 3) {
            throw new InvalidArgumentException("coordonnée invalide : \"$text\"");
        }
        return new static(...array_map('floatval', $parts));
    }
}

$c = Coordinate::fromString('3,4'); // appel sans instance

La création d'une coordonnée à partir d'une chaîne est alors rattachée au concept de coordonnée et son nom explicite l'intention (fromString).

Variantes

Méthodes de fabrique statiques

Contrairement à un constructeur, une méthode de fabrique statique :

Voir aussi le patron Factory.

Fonctions utilitaires

Les méthodes statiques permettent aussi de regrouper des fonctions utilitaires qui ne dépendent d'aucune instance (ex : Math.max(a, b) en Java et en JavaScript, Collections.min en Java).

Méthodes de classe en Python

En Python, nous distinguerons :

  • @staticmethod : une fonction rangée dans la classe, sans accès à la classe.
  • @classmethod : une méthode recevant la classe en premier paramètre (cls). Utilisée pour from_string, elle crée une instance de la sous-classe lorsqu'elle est appelée sur celle-ci (ex : Point3D.from_string(...) renvoie un Point3D).

Mises en garde

  • Une méthode statique ne bénéficie pas du polymorphisme : l'appel Coordinate.fromString(...) désigne une classe précise et ne pourra pas être substitué (ex : par une implémentation de test). Un code qui appelle des méthodes statiques d'autres classes dépend directement de ces classes.
  • Les classes regroupant de nombreuses fonctions utilitaires sans rapport entre elles (ex : Utils, Helpers) sont à éviter : chaque fonction devra être rattachée au concept qu'elle manipule.
  • En TypeScript, JavaScript et Python, une fonction exportée par un module sera parfois plus simple qu'une classe ne contenant que des méthodes statiques.

Liens utiles