🔍 Exercices

Identifier et Résoudre

Consignes :

  • Pour chaque code : identifiez ce qui ne va pas
  • Ensuite : expliquez ce qu'on devrait faire

Exercice 1


class OrderProcessor {
    private MySQLOrderRepository $repo;
    private StripePaymentGateway $gateway;
    
    public function __construct() {
        $this->repo = new MySQLOrderRepository();
        $this->gateway = new StripePaymentGateway('api_key_123');
    }
    
    public function process(Order $order): void {
        $this->gateway->charge($order->getTotal());
        $this->repo->save($order);
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Dépendances concrètes instanciées dans le constructeur

Ce qu'on devrait faire

Injecter des interfaces via le constructeur au lieu de créer les dépendances :

  • CrĂ©er OrderRepositoryInterface et PaymentGatewayInterface
  • Injecter ces interfaces dans le constructeur
  • Permet de changer facilement l'implĂ©mentation (tests, autre BDD, autre gateway)

Principe violé : Dependency Inversion Principle (DIP)

Exercice 2


class Report {
    private array $data;
    
    public function __construct(array $data) {
        $this->data = $data;
    }
    
    public function calculate(): array {
        $total = array_sum($this->data);
        $average = $total / count($this->data);
        return ['total' => $total, 'average' => $average];
    }
    
    public function exportToCSV(string $filename): void {
        $fp = fopen($filename, 'w');
        foreach ($this->data as $row) {
            fputcsv($fp, $row);
        }
        fclose($fp);
    }
    
    public function sendByEmail(string $to): void {
        $stats = $this->calculate();
        mail($to, 'Report', json_encode($stats));
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Trop de responsabilités dans une seule classe

Ce qu'on devrait faire

Séparer les responsabilités en plusieurs classes :

  • ReportCalculator : calculs statistiques
  • CSVExporter : export fichier
  • EmailSender ou ReportMailer : envoi email

Chaque classe aura une seule raison de changer.

Principe violé : Single Responsibility Principle (SRP)

Exercice 3


class PasswordValidator {
    public function validate(string $password): array {
        $errors = [];
        
        if (strlen($password) < 8) {
            $errors[] = "Minimum 8 caractères";
        }
        
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "Au moins une majuscule";
        }
        
        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "Au moins un chiffre";
        }
        
        if (!preg_match('/[!@#$%^&*]/', $password)) {
            $errors[] = "Au moins un caractère spécial";
        }
        
        return $errors;
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Aucun problème !

Explication

Cette classe a une seule responsabilité : valider un mot de passe selon des règles.

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

La classe ne changerait que si les règles de validation changent, ce qui est normal.

Exercice 4


class ShippingCostCalculator {
    public function calculate(float $weight, string $country): float {
        if ($country === 'FR') {
            return $weight * 2.5;
        } elseif ($country === 'DE') {
            return $weight * 3.0;
        } elseif ($country === 'ES') {
            return $weight * 2.8;
        } elseif ($country === 'IT') {
            return $weight * 3.2;
        } else {
            return $weight * 5.0;
        }
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Modification nécessaire pour chaque nouveau pays

Ce qu'on devrait faire

Rendre extensible sans modification :

  • CrĂ©er une interface ShippingStrategyInterface
  • Une classe par pays : FranceShipping, GermanyShipping, etc.
  • Injecter la stratĂ©gie appropriĂ©e ou utiliser un registry/factory

Ajouter un nouveau pays ne nécessite plus de modifier cette classe.

Principe violé : Open/Closed Principle (OCP)

Exercice 5


interface CloudStorage {
    public function upload(string $file): void;
    public function download(string $file): string;
    public function delete(string $file): void;
    public function share(string $file, array $users): string;
    public function setPermissions(string $file, array $perms): void;
    public function getMetadata(string $file): array;
}

class SimpleFileStorage implements CloudStorage {
    public function upload(string $file): void { /* ... */ }
    public function download(string $file): string { /* ... */ }
    public function delete(string $file): void { /* ... */ }
    
    public function share(string $file, array $users): string {
        throw new Exception("Partage non supporté");
    }
    
    public function setPermissions(string $file, array $perms): void {
        throw new Exception("Permissions non supportées");
    }
    
    public function getMetadata(string $file): array {
        return [];
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Interface trop large, méthodes forcées inutilement

Ce qu'on devrait faire

Séparer en interfaces plus petites :

  • BasicStorageInterface : upload, download, delete
  • ShareableInterface : share
  • PermissionableInterface : setPermissions
  • MetadataInterface : getMetadata

Chaque implémentation choisit les interfaces dont elle a vraiment besoin.

Principe violé : Interface Segregation Principle (ISP)

Exercice 6


class PaymentMethod {
    public function charge(float $amount): bool {
        // Logique de paiement
        return true;
    }
    
    public function refund(float $amount): bool {
        // Logique de remboursement
        return true;
    }
}

class GiftCardPayment extends PaymentMethod {
    public function charge(float $amount): bool {
        return parent::charge($amount);
    }
    
    public function refund(float $amount): bool {
        throw new Exception("Les cartes cadeaux ne peuvent pas être remboursées");
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Sous-classe brise le contrat de la classe parent

Ce qu'on devrait faire

Revoir la hiérarchie pour ne pas forcer des comportements incompatibles :

  • CrĂ©er ChargeableInterface et RefundableInterface sĂ©parĂ©es
  • GiftCardPayment implĂ©mente seulement ChargeableInterface
  • Les autres moyens de paiement implĂ©mentent les deux

On ne peut plus substituer GiftCardPayment lĂ  oĂą un remboursement est attendu.

Principe violé : Liskov Substitution Principle (LSP)

Exercice 7


class User {
    private string $name;
    private string $email;
    private string $password;
    
    public function __construct(string $name, string $email, string $password) {
        $this->name = $name;
        $this->email = $email;
        $this->password = password_hash($password, PASSWORD_BCRYPT);
    }
    
    public function save(): void {
        $db = new PDO('mysql:host=localhost;dbname=app', 'root', '');
        $stmt = $db->prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)');
        $stmt->execute([$this->name, $this->email, $this->password]);
    }
    
    public function sendWelcomeEmail(): void {
        $subject = "Bienvenue {$this->name}!";
        $body = "Votre compte a été créé avec succès.";
        mail($this->email, $subject, $body);
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Entité mélangée avec persistence et communication

Ce qu'on devrait faire

Séparer les responsabilités :

  • User : simple entitĂ© avec donnĂ©es
  • UserRepository : gestion de la persistence
  • WelcomeMailer : envoi d'emails

Une entité ne devrait jamais connaître la base de données ou le système de mailing.

Principe violé : Single Responsibility Principle (SRP)

Exercice 8


class NotificationService {
    private TwilioSMSSender $sms;
    private SendGridEmailer $email;
    
    public function __construct() {
        $this->sms = new TwilioSMSSender('account_id', 'token');
        $this->email = new SendGridEmailer('api_key');
    }
    
    public function notifyUser(User $user, string $message): void {
        if ($user->getPreference() === 'sms') {
            $this->sms->send($user->getPhone(), $message);
        } else {
            $this->email->send($user->getEmail(), 'Notification', $message);
        }
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Couplage fort avec des services externes concrets

Ce qu'on devrait faire

Abstraire les dépendances :

  • CrĂ©er SMSSenderInterface et EmailSenderInterface
  • Injecter ces interfaces via le constructeur
  • Configuration et instanciation dĂ©lĂ©guĂ©es Ă  un conteneur DI

Facilite tests, changements de providers, et découplage.

Principe violé : Dependency Inversion Principle (DIP)

Exercice 9


class TaxCalculator {
    public function calculate(float $amount, string $type): float {
        if ($type === 'food') {
            return $amount * 0.055;
        } elseif ($type === 'electronics') {
            return $amount * 0.20;
        } elseif ($type === 'clothing') {
            return $amount * 0.10;
        } elseif ($type === 'books') {
            return $amount * 0.025;
        }
        return $amount * 0.20; // default
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Chaque nouvelle catégorie nécessite une modification

Ce qu'on devrait faire

Utiliser le polymorphisme ou la configuration :

  • Option 1 : Interface TaxStrategyInterface avec classes par catĂ©gorie
  • Option 2 : Table de configuration (BDD ou array) avec les taux par catĂ©gorie
  • Option 3 : Strategy pattern avec factory

Ajouter une catégorie devient une extension, pas une modification.

Principe violé : Open/Closed Principle (OCP)

Exercice 10


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

class ProductRepository implements Repository {
    public function find(int $id) {
        // Recherche en BDD
    }
    
    public function findAll(): array {
        // Retourne tous les produits
    }
    
    public function save($entity): void {
        // Sauvegarde le produit
    }
    
    public function delete($entity): void {
        // Supprime le produit
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Aucun problème !

Explication

Cette interface définit un contrat cohérent pour un repository CRUD complet.

Toutes les méthodes sont logiquement liées à la même responsabilité : gérer la persistence d'entités.

ISP est violé quand on force des implémentations à avoir des méthodes qu'elles ne peuvent pas/ne doivent pas avoir.

Ici, un ProductRepository a besoin de toutes ces méthodes.

Exercice 11


class Discount {
    protected float $percentage;
    
    public function __construct(float $percentage) {
        $this->percentage = $percentage;
    }
    
    public function apply(float $price): float {
        return $price * (1 - $this->percentage);
    }
}

class BlackFridayDiscount extends Discount {
    public function apply(float $price): float {
        // Black Friday: remise fixe de 50€ au lieu de pourcentage
        return $price - 50;
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Comportement fondamentalement différent, substitution impossible

Ce qu'on devrait faire

Ne pas utiliser l'héritage pour des comportements incompatibles :

  • CrĂ©er une interface DiscountInterface
  • Classes sĂ©parĂ©es : PercentageDiscount et FixedAmountDiscount
  • Ou utiliser le pattern Strategy

Le code utilisant Discount s'attend Ă  un calcul en pourcentage, pas en montant fixe.

Principe violé : Liskov Substitution Principle (LSP)

Exercice 12


interface Employee {
    public function work(): void;
    public function attendMeeting(): void;
    public function submitExpenses(): void;
    public function approveLeave(int $employeeId): void;
    public function conductPerformanceReview(int $employeeId): void;
}

class Intern implements Employee {
    public function work(): void { /* ... */ }
    public function attendMeeting(): void { /* ... */ }
    public function submitExpenses(): void { /* ... */ }
    
    public function approveLeave(int $employeeId): void {
        throw new Exception("Interns cannot approve leave");
    }
    
    public function conductPerformanceReview(int $employeeId): void {
        throw new Exception("Interns cannot conduct reviews");
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Interface impose des capacités managériales à tous

Ce qu'on devrait faire

Séparer les interfaces selon les rôles :

  • WorkerInterface : work, attendMeeting, submitExpenses
  • ManagerInterface : approveLeave, conductPerformanceReview

Les managers implémentent les deux, les stagiaires uniquement Worker.

Principe violé : Interface Segregation Principle (ISP)

Exercice 13


class BlogPost {
    private string $title;
    private string $content;
    private DateTime $publishedAt;
    
    public function publish(): void {
        $this->publishedAt = new DateTime();
        
        $db = new PDO('mysql:host=localhost;dbname=blog', 'root', '');
        $stmt = $db->prepare('UPDATE posts SET published_at = ? WHERE id = ?');
        $stmt->execute([$this->publishedAt->format('Y-m-d H:i:s'), $this->id]);
        
        $this->sendToSubscribers();
        $this->updateSearchIndex();
        $this->clearCache();
    }
    
    private function sendToSubscribers(): void { /* ... */ }
    private function updateSearchIndex(): void { /* ... */ }
    private function clearCache(): void { /* ... */ }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Entité orchestrant trop de services externes

Ce qu'on devrait faire

Extraire les services et orchestrer depuis l'extérieur :

  • BlogPost : simple entitĂ© avec donnĂ©es
  • BlogPostRepository : persistence
  • PublicationService : orchestration de la publication
  • NotificationService, SearchIndexer, CacheManager : services spĂ©cialisĂ©s

L'entité ne devrait jamais connaître tous ces détails d'infrastructure.

Principe violé : Single Responsibility Principle (SRP)

Exercice 14


class WeatherService {
    public function getWeather(string $city): array {
        $api = new OpenWeatherMapAPI();
        $api->setKey('123456789');
        $data = $api->fetch($city);
        
        return [
            'temperature' => $data['temp'],
            'humidity' => $data['humidity'],
            'description' => $data['weather'][0]['description']
        ];
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Couplage à une API spécifique + impossible d'en changer

Ce qu'on devrait faire

Abstraire et injecter le provider :

  • CrĂ©er WeatherProviderInterface
  • ImplĂ©menter OpenWeatherMapProvider, possibilitĂ© d'ajouter d'autres
  • Injecter via constructeur

Double bénéfice : testable + extensible sans modification.

Principes violés : DIP (dépendance concrète) + OCP (changer de provider = modifier la classe)

Exercice 15


class FileReader {
    public function read(string $path): string {
        if (!file_exists($path)) {
            throw new FileNotFoundException($path);
        }
        return file_get_contents($path);
    }
}

class NetworkFileReader extends FileReader {
    public function read(string $url): string {
        // Lit un fichier distant via HTTP
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        $result = curl_exec($ch);
        
        if ($result === false) {
            throw new NetworkException("Cannot read from URL");
        }
        
        return $result;
    }
}
                    

Qu'est-ce qui ne va pas ? Que devrait-on faire ?

Problème

Préconditions et comportements différents (exception différente, timeout)

Ce qu'on devrait faire

Ne pas étendre si le comportement est fondamentalement différent :

  • CrĂ©er une interface ReaderInterface
  • Deux classes indĂ©pendantes : LocalFileReader et RemoteFileReader

Le code utilisant FileReader s'attend Ă  FileNotFoundException, pas Ă  NetworkException.

Le timeout de 30s change aussi le comportement attendu.

Principe violé : Liskov Substitution Principle (LSP)

🎉 Fin des Exercices Rapides

PrĂŞts pour les cas concrets ?

Vous allez maintenant travailler en petits groupes sur des scénarios plus complexes.