🎯 Quiz

Échauffement interactif

Consignes :

  • N'hĂ©sitez pas Ă  demander des clarifications si une question n'est pas claire

Question 1


class UserManager {
    public function createUser(string $name, string $email): void {
        $user = new User($name, $email);
        
        $db = new PDO('mysql:host=localhost;dbname=app', 'root', '');

        $stmt = $db->prepare('INSERT INTO users (name, email) VALUES (?, ?)');

        $stmt->execute([$name, $email]);
        
        mail($email, 'Bienvenue', 'Votre compte a été créé');
        
        error_log("User created: $name");
    }
}
                    

Combien de responsabilités a cette classe ?

Réponse

(SRP) 4 responsabilités

  1. Création d'objet User
  2. Accès base de données
  3. Envoi d'email
  4. Logging

Chaque responsabilité représente une raison de changer. Si on change le système de mail, de BDD ou de logging, cette classe doit changer.

Question 2


interface Animal {
    public function manger(): void;
    public function dormir(): void;
    public function voler(): void;
}

class Chien implements Animal {
    public function manger(): void { /* ... */ }
    public function dormir(): void { /* ... */ }
    public function voler(): void {
        throw new Exception("Les chiens ne volent pas");
    }
}
                    

Quel est le problème principal ici ?

Réponse

(OCP) Interface trop large / forcée

L'interface Animal oblige tous les animaux à implémenter voler(), même ceux qui ne volent pas. C'est une violation du principe de ségrégation des interfaces (ISP).

Solution : Créer des interfaces plus spécifiques comme VolantInterface.

Question 3


class Rectangle {
    protected int $largeur;
    protected int $hauteur;
    
    public function setLargeur(int $l): void { $this->largeur = $l; }
    public function setHauteur(int $h): void { $this->hauteur = $h; }
    public function aire(): int { return $this->largeur * $this->hauteur; }
}

class Carre extends Rectangle {
    public function setLargeur(int $l): void {
        $this->largeur = $l;
        $this->hauteur = $l;
    }
    public function setHauteur(int $h): void {
        $this->largeur = $h;
        $this->hauteur = $h;
    }
}
                    

Que se passe-t-il si on remplace Rectangle par Carré ?

Réponse

(LSP) Comportement inattendu / Substitution impossible

Si du code attend un Rectangle et définit séparément largeur et hauteur, le Carré change les deux simultanément. Le comportement attendu est brisé.

$rect->setLargeur(5); $rect->setHauteur(3);

devrait donner aire=25, 15 ou 9?

Violation du principe de substitution de Liskov (LSP).

Question 4


class ReportGenerator {
    public function generate(array $data, string $format): string {
        if ($format === 'pdf') {
            return $this->generatePDF($data);
        } elseif ($format === 'excel') {
            return $this->generateExcel($data);
        } elseif ($format === 'csv') {
            return $this->generateCSV($data);
        }
        throw new Exception("Format non supporté");
    }
}
                    

Que faut-il faire pour ajouter un nouveau format ?

Réponse

(OCP) Modifier la classe existante

Chaque nouveau format nécessite de modifier la méthode generate() en ajoutant un nouveau elseif.

Violation du principe Ouvert/Fermé (OCP) : la classe devrait être fermée à la modification.

Solution : Utiliser une stratégie ou des classes dédiées par format.

Question 5


class OrderProcessor {
    private MySQLDatabase $db;
    
    public function __construct() {
        $this->db = new MySQLDatabase();
    }
    
    public function process(Order $order): void {
        $this->db->save($order);
    }
}
                    

Pourquoi cette classe est-elle difficile Ă  tester ?

Réponse

(DIP) Dépendance concrète / instanciation interne

La classe crée elle-même son instance de MySQLDatabase. On ne peut pas la tester sans vraie base de données, ni utiliser un mock.

Violation du principe d'inversion de dépendance (DIP).

Solution : Injecter l'interface DatabaseInterface via le constructeur.

Question 6


class Product {
    private string $name;
    private float $price;
    
    public function calculateDiscount(): float {
        $today = date('N');
        if ($today == 3) { // Mercredi
            return $this->price * 0.10;
        }
        return 0;
    }
}
                    

Est-ce que cette responsabilité appartient à Product ?

Réponse

(SRP) Non

Le calcul de remise basé sur la date est une règle métier externe au concept de "produit".

Si les règles de promotion changent, la classe Product doit changer, alors que ce n'est pas sa responsabilité.

Solution : Déléguer à une classe PromotionService ou DiscountCalculator.

Question 7


class Logger {
    public function log(string $message, string $level): void {
        $date = date('Y-m-d H:i:s');
        $formatted = "[$date][$level] $message\n";
        file_put_contents('/var/log/app.log', $formatted, FILE_APPEND);
    }
}
                    

Combien de responsabilités ici ?

Réponse

(SRP) 2 responsabilités

  1. Formatage du message log
  2. Écriture dans le fichier

Si on veut changer le format ou le système de stockage, la classe change pour deux raisons différentes.

Question 8


interface Worker {
    public function work(): void;
    public function eat(): void;
}

class Robot implements Worker {
    public function work(): void { /* ... */ }
    public function eat(): void {
        // Un robot ne mange pas
    }
}
                    

Y a-t-il un problème avec cette interface ?

Réponse

(ISP) Oui, méthode inutile forcée

L'interface Worker force Robot à implémenter eat() qui n'a aucun sens pour lui.

Violation de ISP (Interface Segregation Principle).

Solution : Séparer en Workable et Feedable.

Question 9


class PaymentProcessor {
    public function process(float $amount, string $method): void {
        if ($method === 'stripe') {
            $stripe = new StripeAPI();
            $stripe->charge($amount);
        } elseif ($method === 'paypal') {
            $paypal = new PayPalAPI();
            $paypal->pay($amount);
        }
    }
}
                    

Quel principe est violé ?

Réponse

Ouvert/Fermé (OCP) et Inversion de dépendance (DIP)

OCP : Ajouter un nouveau moyen de paiement nécessite de modifier la classe.

DIP : Dépendance directe sur des classes concrètes (StripeAPI, PayPalAPI).

Solution : Injecter une interface PaymentGatewayInterface.

Question 10


class Bird {
    public function fly(): void {
        echo "Je vole";
    }
}

class Penguin extends Bird {
    public function fly(): void {
        throw new Exception("Les pingouins ne volent pas");
    }
}
                    

Quel principe est violé ?

Réponse

Substitution de Liskov (LSP)

On ne peut pas remplacer Bird par Penguin sans casser le comportement attendu.

Si du code appelle fly() sur un Bird, il ne s'attend pas Ă  une exception.

Solution : Revoir la hiérarchie (interface Flyable séparée).

Question 11


class Invoice {
    private array $items;
    private float $total;
    
    public function calculateTotal(): void {
        $this->total = array_sum(array_column($this->items, 'price'));
    }
    
    public function generatePDF(): string {
        return "PDF content of invoice";
    }
    
    public function sendByEmail(string $to): void {
        mail($to, "Invoice", $this->generatePDF());
    }
}
                    

Combien de raisons de changer ?

Réponse

3 raisons

  1. Calcul du total (règles métier)
  2. Génération PDF (format de présentation)
  3. Envoi email (système de communication)

Violation claire du SRP.

Question 12


class EmailService {
    private SMTPMailer $mailer;
    
    public function __construct() {
        $this->mailer = new SMTPMailer('smtp.gmail.com', 587);
    }
    
    public function send(string $to, string $subject, string $body): void {
        $this->mailer->send($to, $subject, $body);
    }
}
                    

Quel est le problème de couplage ici ?

Réponse

Couplage fort / dépendance concrète

EmailService est couplé directement à SMTPMailer. Impossible de changer de système d'envoi ou de tester facilement.

Violation du DIP (Dependency Inversion Principle).

Solution : Dépendre d'une interface MailerInterface et injecter l'implémentation.

Question 13


class ShoppingCart {
    private array $items = [];
    
    public function addItem(Product $product): void {
        $this->items[] = $product;
    }
    
    public function getTotal(): float {
        return array_sum(array_map(fn($p) => $p->getPrice(), $this->items));
    }
}
                    

Cette classe respecte-t-elle le SRP ?

Réponse

Oui

Cette classe a une seule responsabilité claire : gérer le contenu du panier.

Ajouter des items et calculer le total sont deux aspects de cette même responsabilité.

Important : Plusieurs méthodes ≠ plusieurs responsabilités.

Question 14


abstract class Shape {
    abstract public function area(): float;
}

class Circle extends Shape {
    public function __construct(private float $radius) {}
    public function area(): float {
        return pi() * $this->radius ** 2;
    }
}

class Square extends Shape {
    public function __construct(private float $side) {}
    public function area(): float {
        return $this->side ** 2;
    }
}
                    

Ce design respecte-t-il LSP ?

Réponse

Oui

On peut remplacer Shape par n'importe quelle sous-classe sans problème.

Chaque forme respecte le contrat : calculer une aire et retourner un float.

Le comportement est prévisible et cohérent.

Question 15


class NotificationService {
    public function notify(User $user, string $message): void {
        if ($user->getPreference() === 'email') {
            $this->sendEmail($user->getEmail(), $message);
        } elseif ($user->getPreference() === 'sms') {
            $this->sendSMS($user->getPhone(), $message);
        } elseif ($user->getPreference() === 'push') {
            $this->sendPush($user->getDeviceId(), $message);
        }
    }
}
                    

Que se passe-t-il si on ajoute Slack ?

Réponse

Il faut modifier la classe

Chaque nouveau canal nécessite d'ajouter un elseif dans cette méthode.

Violation du principe Ouvert/Fermé (OCP).

Solution : Utiliser le pattern Strategy avec des notifiers injectés.

Question 16


interface Repository {
    public function find(int $id);
    public function findAll(): array;
    public function save($entity): void;
    public function delete($entity): void;
    public function findByComplexCriteria(array $criteria): array;
}

class ReadOnlyUserRepository implements Repository {
    public function find(int $id) { /* ... */ }
    public function findAll(): array { /* ... */ }
    public function save($entity): void {
        throw new Exception("Read-only");
    }
    public function delete($entity): void {
        throw new Exception("Read-only");
    }
    public function findByComplexCriteria(array $c): array { /* ... */ }
}
                    

Quel problème voyez-vous ?

Réponse

Interface trop large / méthodes forcées

Le repository read-only est forcé d'implémenter save() et delete() qu'il ne peut pas utiliser.

Violation de ISP (Interface Segregation Principle).

Solution : Séparer en ReadableRepository et WritableRepository.

Question 17


class FileUploader {
    public function upload(string $path): void {
        if (!$this->isValidSize($path)) {
            throw new Exception("Fichier trop grand");
        }
        if (!$this->isValidType($path)) {
            throw new Exception("Type non autorisé");
        }
        $this->moveToStorage($path);
        $this->createThumbnail($path);
        $this->updateDatabase($path);
    }
}
                    

Combien de responsabilités ?

Réponse

4 responsabilités

  1. Validation de fichier
  2. Stockage fichier
  3. Traitement image (thumbnail)
  4. Persistence en base

Violation claire du SRP.

Question 18


class DiscountCalculator {
    public function calculate(float $price, string $customerType): float {
        return match($customerType) {
            'vip' => $price * 0.8,
            'regular' => $price * 0.95,
            'new' => $price,
            default => throw new Exception("Type inconnu")
        };
    }
}
                    

Comment ajouter un nouveau type de client ?

Réponse

Modifier cette classe

Chaque nouveau type de client nécessite de modifier le match.

Violation du principe Ouvert/Fermé.

Solution : Pattern Strategy ou State avec des classes de réduction par type.

Question 19


class CacheManager {
    private RedisClient $redis;
    
    public function __construct() {
        $this->redis = new RedisClient('localhost', 6379);
    }
    
    public function get(string $key): mixed {
        return $this->redis->get($key);
    }
}
                    

Pourquoi est-ce difficile Ă  tester ?

Réponse

Impossible de mocker / dépendance concrète interne

La classe instancie elle-mĂŞme RedisClient. On ne peut pas injecter un mock pour les tests.

Violation du DIP.

Solution : Injecter une interface CacheInterface via le constructeur.

Question 20


interface Printer {
    public function print(Document $doc): void;
    public function scan(Document $doc): void;
    public function fax(Document $doc): void;
}

class SimplePrinter implements Printer {
    public function print(Document $doc): void { /* ... */ }
    public function scan(Document $doc): void {
        throw new Exception("Pas de scanner");
    }
    public function fax(Document $doc): void {
        throw new Exception("Pas de fax");
    }
}
                    

Quel principe est violé ?

Réponse

Ségrégation des interfaces (ISP)

L'interface force SimplePrinter à implémenter des méthodes inutiles.

Solution : Créer des interfaces séparées : Printable, Scannable, Faxable.

Question 21


class Calculator {
    public function add(int $a, int $b): int {
        return $a + $b;
    }
    
    public function subtract(int $a, int $b): int {
        return $a - $b;
    }
    
    public function multiply(int $a, int $b): int {
        return $a * $b;
    }
}
                    

Cette classe respecte-t-elle le SRP ?

Réponse

Oui

Elle a une seule responsabilité : effectuer des opérations mathématiques.

Les trois méthodes font partie de cette même responsabilité.

Rappel : Plusieurs méthodes liées = une seule responsabilité cohérente.

Question 22


class OrderService {
    public function placeOrder(Order $order): void {
        $this->validateOrder($order);
        
        $db = new MySQLConnection();
        $db->insert('orders', $order->toArray());
        
        $emailer = new SMTPMailer();
        $emailer->send($order->getCustomerEmail(), "Confirmation");
        
        $logger = new FileLogger('/var/log/orders.log');
        $logger->log("Order placed: " . $order->getId());
    }
}
                    

Quels principes sont violés ?

Réponse

SRP et DIP

SRP : Validation, persistence, email, logging = 4 responsabilités.

DIP : Instanciation directe de classes concrètes (MySQLConnection, SMTPMailer, FileLogger).

Double violation fréquente dans le code legacy.

Question 23


interface Vehicle {
    public function startEngine(): void;
    public function stopEngine(): void;
}

class ElectricCar implements Vehicle {
    public function startEngine(): void {
        // Une voiture électrique n'a pas de "moteur" à démarrer
        $this->activateBattery();
    }
    public function stopEngine(): void {
        $this->deactivateBattery();
    }
}
                    

Y a-t-il un problème conceptuel ?

Réponse

Oui, vocabulaire inadapté

L'interface utilise "Engine" qui ne correspond pas aux véhicules électriques.

Bien que techniquement fonctionnel, c'est une violation conceptuelle de LSP : le nom trompe sur le comportement réel.

Solution : Utiliser des méthodes plus génériques comme start() et stop().

Question 24


class UserValidator {
    public function validate(array $data): array {
        $errors = [];
        
        if (empty($data['email'])) {
            $errors[] = "Email requis";
        }
        if (empty($data['password']) || strlen($data['password']) < 8) {
            $errors[] = "Mot de passe invalide";
        }
        if (empty($data['age']) || $data['age'] < 18) {
            $errors[] = "Âge minimum 18 ans";
        }
        
        return $errors;
    }
}
                    

Cette classe respecte-t-elle le SRP ?

Réponse

Oui

Sa seule responsabilité est la validation de données utilisateur.

Les multiples vérifications font toutes partie de cette même responsabilité.

Ne pas confondre complexité et multiple responsabilités.

Question 25


class Database {
    private PDO $connection;
    
    public function __construct(string $host, string $db, string $user, string $pass) {
        $this->connection = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
    }
}

class UserRepository {
    private Database $db;
    
    public function __construct() {
        $this->db = new Database('localhost', 'app', 'root', 'secret');
    }
}
                    

Quel est le problème principal ?

Réponse

Configuration hardcodée / dépendance concrète

UserRepository crée sa propre instance de Database avec des credentials en dur.

Impossible de tester, impossible de changer de BDD, impossible de réutiliser une connexion.

Violation du DIP.

Question 26


class Product {
    private string $name;
    private float $price;
    private int $stock;
    
    public function getName(): string { return $this->name; }
    public function getPrice(): float { return $this->price; }
    public function getStock(): int { return $this->stock; }
    
    public function setName(string $n): void { $this->name = $n; }
    public function setPrice(float $p): void { $this->price = $p; }
    public function setStock(int $s): void { $this->stock = $s; }
}
                    

Cette classe a-t-elle trop de responsabilités ?

Réponse

Non

C'est un simple objet de données (DTO/Entity) qui encapsule les propriétés d'un produit.

Les getters/setters font partie de sa responsabilité unique : représenter un produit.

Attention : Beaucoup de méthodes ≠ beaucoup de responsabilités.

Question 27


class ImageProcessor {
    public function process(string $path, string $operation): void {
        $img = imagecreatefromjpeg($path);
        
        if ($operation === 'resize') {
            $this->resize($img, 800, 600);
        } elseif ($operation === 'grayscale') {
            $this->grayscale($img);
        } elseif ($operation === 'rotate') {
            $this->rotate($img, 90);
        }
        
        imagejpeg($img, $path);
    }
}
                    

Pour ajouter un filtre "blur", que faut-il faire ?

Réponse

Modifier la méthode process

Chaque nouvelle opération nécessite d'ajouter un elseif.

Violation du principe Ouvert/Fermé.

Solution : Pattern Strategy avec des classes d'opération injectables.

Question 28


interface DataSource {
    public function read(): array;
    public function write(array $data): void;
    public function backup(): void;
    public function restore(string $backupId): void;
}

class ApiDataSource implements DataSource {
    public function read(): array { /* Appel API */ }
    public function write(array $data): void { /* Appel API */ }
    public function backup(): void {
        throw new Exception("API ne supporte pas backup");
    }
    public function restore(string $id): void {
        throw new Exception("API ne supporte pas restore");
    }
}
                    

Quel est le problème ?

Réponse

Interface trop large / méthodes inutiles

ApiDataSource ne peut pas faire de backup/restore mais est forcé de les implémenter.

Violation de ISP.

Solution : Séparer en Readable, Writable, Backupable.

Question 29


class Stack {
    private array $items = [];
    
    public function push($item): void {
        $this->items[] = $item;
    }
    
    public function pop() {
        return array_pop($this->items);
    }
}

class LimitedStack extends Stack {
    public function __construct(private int $maxSize) {}
    
    public function push($item): void {
        if (count($this->items) >= $this->maxSize) {
            throw new Exception("Stack pleine");
        }
        parent::push($item);
    }
}
                    

Peut-on substituer Stack par LimitedStack ?

Réponse

Non sans risque

LimitedStack ajoute une précondition (vérification de taille) que Stack n'a pas.

Du code qui utilise Stack ne s'attend pas Ă  une exception sur push().

Violation subtile de LSP.

Question 30


class Logger {
    private string $level;
    private FileWriter $file;
    
    public function __construct() {
        $this->level = 'INFO';
        $this->file = new FileWriter('/var/log/app.log');
    }
    
    public function log(string $message): void {
        $formatted = "[{$this->level}] $message";
        $this->file->write($formatted);
    }
}
                    

Combien de violations identifiez-vous ?

Réponse

2 violations (SRP et DIP)

SRP : Formatage + écriture = 2 responsabilités.

DIP : Création interne de FileWriter, impossible de changer ou tester.

Cas typique où plusieurs principes sont violés simultanément.

🎉 Fin du Quiz

PrĂŞts pour les exercices pratiques ?

N'oubliez pas : les principes SOLID sont des guides, pas des règles absolues.