Aide en ligne
Tous les articles

Architecture métier : des classes pour représenter les entités

Appliquer une architecture métier dans un projet consiste à regrouper, dans un même dossier ou package, des classes qui représentent les entités du projet : des produits, des personnes, des éléments abstraits, tout ce qui découle directement des besoins fonctionnels. Les attributs de chaque classe contiennent les données que le système doit traiter, et ses méthodes correspondent aux fonctionnalités associées à l'entité.

Le principe de conception repose sur une observation : une entité est manipulée de la même façon à de nombreux endroits du projet. En lui consacrant une classe, on centralise ces traitements et on les rend plus faciles à comprendre. C'est la couche « métier » décrite dans l'article Architecture logicielle.

Ce que cela apporte

Mise en œuvre

Pour chaque entité, on crée deux classes :

  1. la classe métier (par exemple Livre), qui représente un objet. Sa responsabilité de base est de se charger depuis la base de données et de s'y sauvegarder, puis d'appliquer les règles qui concernent un seul objet ;
  2. la classe collection (par exemple LivreCollection), qui représente une liste d'objets. Ses responsabilités minimales sont de charger et sauvegarder des listes, et de retrouver un objet dans la liste. Elle porte aussi les traitements qui concernent plusieurs objets : recherche, statistiques, opérations en lot.

Quelques règles aident à garder ces classes saines :

Exemple : la gestion d'une bibliothèque

Dans une bibliothèque municipale, un système informatique assure la gestion des abonnés, des commandes de livres, des stocks, des réservations et des emprunts. Il est multi-utilisateurs et possède deux types d'accès :

Cet exemple se limite à la table Livre et aux classes Livre et LivreCollection.

La table

CREATE TABLE Livre (
    IDLivre            INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    Titre              VARCHAR(255) NOT NULL,
    fkAuteur           INT NOT NULL,
    Resume             TEXT NULL,
    Image              VARCHAR(255) NULL,
    QuantiteEnStock    INT NOT NULL DEFAULT 0,   -- exemplaires en rayon
    QuantiteEnLocal    INT NOT NULL DEFAULT 0,   -- exemplaires à lire sur place uniquement
    QuantiteEnPret     INT NOT NULL DEFAULT 0,   -- exemplaires empruntés
    QuantiteReservee   INT NOT NULL DEFAULT 0,   -- exemplaires mis de côté pour des réservations
    QuantiteEnCommande INT NOT NULL DEFAULT 0,   -- exemplaires commandés, pas encore reçus
    Type               ENUM('suggestion','valide') NOT NULL DEFAULT 'suggestion'
);

La classe métier

Version simplifiée : seuls les champs utiles aux règles sont repris, et la gestion des erreurs techniques est omise.

class Livre
{
    private $id;
    private $titre;
    private $stock = 0;        // exemplaires en rayon
    private $reservee = 0;     // exemplaires mis de côté pour des réservations
    private $enPret = 0;
    private $enCommande = 0;
    private $type = 'suggestion';

    // Un bibliothécaire crée un livre sur son interface ; un visiteur du site suggère un livre à commander.
    public static function creer(PDO $pdo, string $titre, int $idAuteur, string $type = 'suggestion'): Livre
    {
        $st = $pdo->prepare('INSERT INTO Livre (Titre, fkAuteur, Type) VALUES (?, ?, ?)');
        $st->execute(array($titre, $idAuteur, $type));
        return self::charger($pdo, (int) $pdo->lastInsertId());
    }

    // Appelée sur le poste du bibliothécaire comme sur la fiche détaillée du site.
    public static function charger(PDO $pdo, int $id): ?Livre
    {
        $st = $pdo->prepare('SELECT * FROM Livre WHERE IDLivre = ?');
        $st->execute(array($id));
        $ligne = $st->fetch(PDO::FETCH_ASSOC);
        return $ligne ? self::depuisLigne($ligne) : null;
    }

    public static function depuisLigne(array $ligne): Livre
    {
        $livre = new Livre();
        $livre->id = (int) $ligne['IDLivre'];
        $livre->titre = $ligne['Titre'];
        $livre->stock = (int) $ligne['QuantiteEnStock'];
        $livre->reservee = (int) $ligne['QuantiteReservee'];
        $livre->enPret = (int) $ligne['QuantiteEnPret'];
        $livre->enCommande = (int) $ligne['QuantiteEnCommande'];
        $livre->type = $ligne['Type'];
        return $livre;
    }

    // Exemplaires qu'on peut encore emprunter ou réserver.
    public function exemplairesLibres(): int
    {
        return $this->stock - $this->reservee;
    }

    // Lire sur place ou emporter à la maison.
    public function emprunter(string $typeEmprunt): void
    {
        if ($typeEmprunt === 'a_la_maison') {
            if ($this->exemplairesLibres() < 1) {
                throw new RegleMetierException('Aucun exemplaire n\'est disponible.');
            }
            $this->stock--;
            $this->enPret++;
        }
    }

    public function ramener(): void
    {
        if ($this->enPret < 1) {
            throw new RegleMetierException('Aucun exemplaire de ce livre n\'est en prêt.');
        }
        $this->enPret--;
        $this->stock++;
    }

    // Un visiteur du site réserve en ligne ; à l'accueil, le bibliothécaire peut saisir la même réservation.
    public function reserver(): void
    {
        if ($this->exemplairesLibres() < 1) {
            throw new RegleMetierException('Tous les exemplaires sont déjà réservés ou empruntés.');
        }
        $this->reservee++;
    }

    // Un bibliothécaire décide d'acheter des exemplaires.
    public function commander(int $quantite): void
    {
        if ($quantite < 1) {
            throw new RegleMetierException('La quantité commandée doit être positive.');
        }
        $this->enCommande += $quantite;
    }

    // Lors d'un inventaire, les quantités sont mises à jour (lecteur de codes-barres, par exemple).
    public function sauvegarder(PDO $pdo): void
    {
        $st = $pdo->prepare(
            'UPDATE Livre SET Titre = ?, QuantiteEnStock = ?, QuantiteReservee = ?, QuantiteEnPret = ?,
                              QuantiteEnCommande = ?, Type = ? WHERE IDLivre = ?'
        );
        $st->execute(array($this->titre, $this->stock, $this->reservee, $this->enPret, $this->enCommande, $this->type, $this->id));
    }

    // Par exemple pour supprimer un doublon.
    public function supprimer(PDO $pdo): void
    {
        $pdo->prepare('DELETE FROM Livre WHERE IDLivre = ?')->execute(array($this->id));
    }
}

class RegleMetierException extends Exception {}

La classe collection

class LivreCollection
{
    private $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    // Moteur de recherche du site et de l'interface des bibliothécaires.
    public function rechercher(string $motsDuTitre = '', int $limite = 20, int $decalage = 0): array
    {
        $st = $this->pdo->prepare('SELECT * FROM Livre WHERE Titre LIKE ? ORDER BY Titre LIMIT ' . $limite . ' OFFSET ' . $decalage);
        $st->execute(array('%' . $motsDuTitre . '%'));
        return array_map(array('Livre', 'depuisLigne'), $st->fetchAll(PDO::FETCH_ASSOC));
    }

    // Statistiques : certaines sont visibles sur le site, d'autres seulement dans l'interface d'administration.
    public function compter(string $type = 'valide'): int
    {
        $st = $this->pdo->prepare('SELECT COUNT(*) FROM Livre WHERE Type = ?');
        $st->execute(array($type));
        return (int) $st->fetchColumn();
    }

    // Réception d'un lot de livres commandés : array(idLivre => quantité reçue).
    public function recevoir(array $quantitesParLivre): void
    {
        $this->pdo->beginTransaction();
        $st = $this->pdo->prepare(
            'UPDATE Livre SET QuantiteEnStock = QuantiteEnStock + ?, QuantiteEnCommande = QuantiteEnCommande - ?
             WHERE IDLivre = ? AND QuantiteEnCommande >= ?'
        );
        foreach ($quantitesParLivre as $idLivre => $quantite) {
            $st->execute(array($quantite, $quantite, $idLivre, $quantite));
            if ($st->rowCount() !== 1) {
                $this->pdo->rollBack();
                throw new RegleMetierException('Quantité reçue incohérente pour le livre ' . $idLivre . '.');
            }
        }
        $this->pdo->commit();
    }
}

La même classe, deux interfaces

C'est tout l'intérêt de la démarche : le site public et l'interface des bibliothécaires utilisent les mêmes objets, donc les mêmes règles.

// Site public : un abonné réserve un livre
$livre = Livre::charger($pdo, (int) $_POST['livre']);
try {
    $livre->reserver();
    $livre->sauvegarder($pdo);
    echo 'Réservation enregistrée.';
} catch (RegleMetierException $e) {
    echo htmlspecialchars($e->getMessage());
}

// Interface du bibliothécaire : réception d'un lot
$collection = new LivreCollection($pdo);
$collection->recevoir(array(12 => 3, 48 => 1));

Lire une classe comme un sommaire

En parcourant la liste des méthodes de Livre et de LivreCollection, un développeur découvre d'un coup d'œil l'étendue des fonctionnalités de l'entité : on peut créer, charger, emprunter, ramener, réserver, commander, sauvegarder et supprimer un livre, rechercher et compter des livres, et recevoir des livres commandés. Il comprend ainsi non seulement le code, mais aussi les processus de fonctionnement du système. C'est le grand intérêt des classes métier.

Les limites, et comment les gérer

Pour aller plus loin