Single Responsibility
Une classe ne doit avoir qu'une seule raison de changer. Chaque classe doit encapsuler une seule responsabilité et la remplir entièrement.
// Cette classe fait TOUT : validation,
// persistance ET envoi d'emails
class UserManager
{
public function register(string $email, string $password): void
{
// Validation
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email invalide');
}
// Sauvegarde en base
$pdo = new \PDO('mysql:host=localhost');
$stmt = $pdo->prepare('INSERT INTO users ...');
$stmt->execute([$email, $password]);
// Envoi d'email
mail($email, 'Bienvenue!', '...');
}
}
// Chaque classe = une responsabilité
class UserValidator
{
public function validate(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
}
class UserRepository
{
public function save(User $user): void { /* ... */ }
}
class WelcomeMailer
{
public function send(User $user): void { /* ... */ }
}
Astuce : Si vous décrivez votre classe avec « ET » (elle valide ET sauvegarde ET envoie), c'est un signe qu'elle a trop de responsabilités.
Open / Closed
Une classe doit être ouverte à l'extension mais fermée à la modification. On ajoute du comportement sans toucher au code existant.
// Chaque nouveau type de remise
// oblige à modifier cette classe
class DiscountCalculator
{
public function calculate(
Order $order,
string $type
): float {
return match($type) {
'percentage' => $order->total * 0.1,
'fixed' => $order->total - 5,
'vip' => $order->total * 0.2,
// ... encore et encore
};
}
}
interface Discount
{
public function apply(Order $order): float;
}
class PercentageDiscount implements Discount
{
public function __construct(
private readonly float $rate
) {}
public function apply(Order $order): float
{
return $order->total * $this->rate;
}
}
// Ajouter un nouveau type ? Créez une classe !
class VipDiscount implements Discount { /* ... */ }
Pattern : Utilisez des interfaces et le Strategy Pattern pour permettre l'extension sans modification du code existant.
Liskov Substitution
Une sous-classe doit pouvoir remplacer sa classe parente sans casser le comportement du programme. Les enfants doivent respecter le « contrat » du parent.
class FileRepository
{
public function read(string $path): string
{
return file_get_contents($path);
}
public function write(string $path, string $data): void
{
file_put_contents($path, $data);
}
}
// Casse le contrat du parent !
class ReadOnlyFileRepo extends FileRepository
{
public function write(string $path, string $data): void
{
throw new \RuntimeException('Interdit !');
}
}
interface Readable
{
public function read(string $path): string;
}
interface Writable
{
public function write(string $path, string $data): void;
}
// Chaque classe respecte son contrat
class ReadOnlyFileRepo implements Readable
{
public function read(string $path): string
{
return file_get_contents($path);
}
}
class FullFileRepo implements Readable, Writable
{
/* read() + write() */
}
Règle d'or : Si une sous-classe lève une exception là où le parent ne le fait pas, ou ignore une méthode héritée, vous violez Liskov.
Interface Segregation
Aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas. Préférez plusieurs petites interfaces spécifiques à une seule interface générale.
// Interface trop large ("fat interface")
interface Worker
{
public function work(): void;
public function eat(): void;
public function sleep(): void;
}
// Un robot ne mange pas et ne dort pas !
class Robot implements Worker
{
public function work(): void { /* OK */ }
public function eat(): void { /* ??? */ }
public function sleep(): void { /* ??? */ }
}
interface Workable
{
public function work(): void;
}
interface Feedable
{
public function eat(): void;
}
interface Sleepable
{
public function sleep(): void;
}
// Chaque classe choisit ce dont elle a besoin
class Human implements Workable, Feedable, Sleepable
{ /* ... */ }
class Robot implements Workable
{ /* Seulement work() */ }
Signal d'alerte : Si vous implémentez une interface et laissez des méthodes vides
ou avec throw new \Exception,
l'interface est trop grosse.
Dependency Inversion
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions (interfaces).
// Couplage fort à une implémentation
class OrderService
{
private MySqlDatabase $db;
private StripePayment $payment;
public function __construct()
{
// Instanciation directe = couplage
$this->db = new MySqlDatabase();
$this->payment = new StripePayment();
}
public function place(Order $order): void
{
$this->db->save($order);
$this->payment->charge($order->total);
}
}
interface Database
{
public function save(object $entity): void;
}
interface PaymentGateway
{
public function charge(float $amount): bool;
}
// Dépend d'abstractions, pas d'implémentations
class OrderService
{
public function __construct(
private readonly Database $db,
private readonly PaymentGateway $payment,
) {}
public function place(Order $order): void
{
$this->db->save($order);
$this->payment->charge($order->total);
}
}
En pratique : Utilisez l'injection de dépendances via le constructeur et le Service Container de votre framework (Laravel, Symfony) pour câbler les implémentations.