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 ?
Dépendances concrètes instanciées dans le constructeur
Injecter des interfaces via le constructeur au lieu de créer les dépendances :
OrderRepositoryInterface et PaymentGatewayInterfacePrincipe violé : Dependency Inversion Principle (DIP)
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 ?
Trop de responsabilités dans une seule classe
Séparer les responsabilités en plusieurs classes :
ReportCalculator : calculs statistiquesCSVExporter : export fichierEmailSender ou ReportMailer : envoi emailChaque classe aura une seule raison de changer.
Principe violé : Single Responsibility Principle (SRP)
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 ?
Aucun problème !
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.
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 ?
Modification nécessaire pour chaque nouveau pays
Rendre extensible sans modification :
ShippingStrategyInterfaceFranceShipping, GermanyShipping, etc.Ajouter un nouveau pays ne nécessite plus de modifier cette classe.
Principe violé : Open/Closed Principle (OCP)
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 ?
Interface trop large, méthodes forcées inutilement
Séparer en interfaces plus petites :
BasicStorageInterface : upload, download, deleteShareableInterface : sharePermissionableInterface : setPermissionsMetadataInterface : getMetadataChaque implémentation choisit les interfaces dont elle a vraiment besoin.
Principe violé : Interface Segregation Principle (ISP)
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 ?
Sous-classe brise le contrat de la classe parent
Revoir la hiérarchie pour ne pas forcer des comportements incompatibles :
ChargeableInterface et RefundableInterface séparéesGiftCardPayment implémente seulement ChargeableInterfaceOn ne peut plus substituer GiftCardPayment là où un remboursement est attendu.
Principe violé : Liskov Substitution Principle (LSP)
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 ?
Entité mélangée avec persistence et communication
Séparer les responsabilités :
User : simple entité avec donnéesUserRepository : gestion de la persistenceWelcomeMailer : envoi d'emailsUne entité ne devrait jamais connaître la base de données ou le système de mailing.
Principe violé : Single Responsibility Principle (SRP)
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 ?
Couplage fort avec des services externes concrets
Abstraire les dépendances :
SMSSenderInterface et EmailSenderInterfaceFacilite tests, changements de providers, et découplage.
Principe violé : Dependency Inversion Principle (DIP)
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 ?
Chaque nouvelle catégorie nécessite une modification
Utiliser le polymorphisme ou la configuration :
TaxStrategyInterface avec classes par catégorieAjouter une catégorie devient une extension, pas une modification.
Principe violé : Open/Closed Principle (OCP)
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 ?
Aucun problème !
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.
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 ?
Comportement fondamentalement différent, substitution impossible
Ne pas utiliser l'héritage pour des comportements incompatibles :
DiscountInterfacePercentageDiscount et FixedAmountDiscountLe code utilisant Discount s'attend Ă un calcul en pourcentage, pas en montant fixe.
Principe violé : Liskov Substitution Principle (LSP)
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 ?
Interface impose des capacités managériales à tous
Séparer les interfaces selon les rôles :
WorkerInterface : work, attendMeeting, submitExpensesManagerInterface : approveLeave, conductPerformanceReviewLes managers implémentent les deux, les stagiaires uniquement Worker.
Principe violé : Interface Segregation Principle (ISP)
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 ?
Entité orchestrant trop de services externes
Extraire les services et orchestrer depuis l'extérieur :
BlogPost : simple entité avec donnéesBlogPostRepository : persistencePublicationService : orchestration de la publicationNotificationService, SearchIndexer, CacheManager : services spécialisésL'entité ne devrait jamais connaître tous ces détails d'infrastructure.
Principe violé : Single Responsibility Principle (SRP)
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 ?
Couplage à une API spécifique + impossible d'en changer
Abstraire et injecter le provider :
WeatherProviderInterfaceOpenWeatherMapProvider, possibilité d'ajouter d'autresDouble bénéfice : testable + extensible sans modification.
Principes violés : DIP (dépendance concrète) + OCP (changer de provider = modifier la classe)
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 ?
Préconditions et comportements différents (exception différente, timeout)
Ne pas étendre si le comportement est fondamentalement différent :
ReaderInterfaceLocalFileReader et RemoteFileReaderLe code utilisant FileReader s'attend Ă FileNotFoundException, pas Ă NetworkException.
Le timeout de 30s change aussi le comportement attendu.
Principe violé : Liskov Substitution Principle (LSP)
Vous allez maintenant travailler en petits groupes sur des scénarios plus complexes.