Les Interfaces en Programmation Orientée Objet : Principe SOLID et Exemple Pratique #
Introduction #
Dans cet article, nous allons explorer le principe Interface Segregation Principle (ISP) des principes SOLID à travers un exemple concret de système de paiement. Nous verrons comment les interfaces permettent de créer du code flexible, maintenable et respectueux des bonnes pratiques de conception.
Qu’est-ce qu’une Interface ? #
Une interface définit un contrat que les classes doivent respecter. Elle spécifie quoi faire sans préciser comment le faire. C’est un concept fondamental de l’abstraction en POO.
Avantages des interfaces #
- Découplage : Réduction des dépendances entre les composants
- Flexibilité : Facilite l’ajout de nouvelles implémentations
- Testabilité : Permet l’injection de dépendances et les mocks
- Polymorphisme : Une même interface, plusieurs implémentations
Étude de Cas : Système de Paiement #
Imaginons un système e-commerce nécessitant plusieurs méthodes de paiement. Voici comment structurer le code avec des interfaces.
Architecture Globale - Problème Initial #
classDiagram
class Payment {
<>
+processPayment(amount: number) void
+refund(amount: number) void
+saveCard() number
+authorize3DS() void
+validateIBAN() void
+sendSMS() void
}
Payment <|-- CreditCardPayment
Payment <|-- PayPalPayment
Payment <|-- CashPayment
class CreditCardPayment {
-cardNumber: string
-cvv: string
+processPayment(amount: number) void
+refund(amount: number) void
+saveCard() void
+authorize3DS() void
+validateIBAN() void
+sendSMS() void
}
class PayPalPayment {
-email: string
+processPayment(amount: number) void
+refund(amount: number) void
+saveCard() void
+authorize3DS() void
+validateIBAN() void
+sendSMS() void
}
class CashPayment {
+processPayment(amount: number) void
+refund(amount: number) void
+saveCard() void
+authorize3DS() void
+validateIBAN() void
+sendSMS() void
}
Problème : Cette interface viole le principe ISP ! Toutes les méthodes de paiement n’ont pas besoin de toutes ces fonctionnalités.
- PayPal n’a pas besoin de
saveCard(),authorize3DS(),validateIBAN()ousendSMS() - Le paiement cash n’a besoin que de
processPayment()etrefund() - Chaque classe est forcée d’implémenter des méthodes inutiles
Implémentation avec Interface Monolithique #
1. Paiement par Carte de Crédit #
class CreditCardPayment extends Payment {
private cardNumber: string;
private cvv: string;
processPayment(amount: number): void {
console.log('Paiement CB de ' + amount + '€');
}
refund(amount: number): void {
console.log('Remboursement de ' + amount + '€');
}
saveCard(): void {
console.log('Carte enregistrée');
}
authorize3DS(): void {
console.log('Autorisation 3D Secure');
}
validateIBAN(): void {
// N'a pas de sens pour une carte bancaire !
throw new Error('Les cartes n'ont pas d'IBAN');
}
sendSMS(): void {
console.log('SMS envoyé');
}
}
2. Paiement PayPal #
class PayPalPayment extends Payment {
private email: string;
processPayment(amount: number): void {
console.log('Paiement PayPal de ' + amount + '€');
}
refund(amount: number): void {
console.log('Remboursement PayPal de ' + amount + '€');
}
saveCard(): void {
// PayPal ne gère pas les cartes !
throw new Error('PayPal ne gère pas les cartes');
}
authorize3DS(): void {
// PayPal n'utilise pas 3DS !
throw new Error('PayPal n'utilise pas 3DS');
}
validateIBAN(): void {
// PayPal n'utilise pas d'IBAN !
throw new Error('PayPal n'utilise pas d'IBAN');
}
sendSMS(): void {
// PayPal envoie des emails, pas des SMS !
throw new Error('PayPal envoie des emails');
}
}
3. Paiement en Espèces #
class CashPayment extends Payment {
processPayment(amount: number): void {
console.log('Paiement cash de ' + amount + '€');
}
refund(amount: number): void {
console.log('Remboursement cash de ' + amount + '€');
}
saveCard(): void {
// Le cash n'a pas de carte !
throw new Error('Le cash n'a pas de carte');
}
authorize3DS(): void {
// Pas de 3DS pour le cash !
throw new Error('Pas de 3DS pour le cash');
}
validateIBAN(): void {
// Pas d'IBAN pour le cash !
throw new Error('Pas d'IBAN pour le cash');
}
sendSMS(): void {
// Pas de SMS pour le cash !
throw new Error('Pas de SMS pour le cash');
}
}
Conséquences du Mauvais Design #
Ce code présente plusieurs problèmes graves :
- Violations du LSP : Les sous-classes lancent des exceptions pour des méthodes héritées
- Code mort : Beaucoup de méthodes inutilisées qui polluent le code
- Maintenance difficile : Ajouter une nouvelle méthode impacte toutes les classes
- Tests complexes : Nécessité de tester des méthodes qui ne devraient pas exister
- Compréhension difficile : L’API est trompeuse sur ce que chaque classe peut vraiment faire
Problème : Cette interface viole le principe ISP ! Toutes les méthodes de paiement n’ont pas besoin de toutes ces fonctionnalités.
Solution : Ségrégation des Interfaces #
// Interface de base - fonctionnalités communes
interface Payment {
processPayment(amount: number): void;
refund(amount: number): void;
}
// Interface pour la sauvegarde de carte
interface CardStorable {
saveCard(cardDetails: void): void;
}
// Interface pour l'authentification 3D Secure
interface SecureAuthenticable {
authenticate(): boolean;
}
// Interface pour validation IBAN
interface Refundable {
refund(amount: number): void;
}
Implémentation des Classes Concrètes #
1. Paiement par Carte de Crédit #
class CreditCardPayment implements Payment, CardStorable, SecureAuthenticable {
private cardNumber: string;
private cvv: number;
processPayment(amount: number): void {
console.log(`Paiement de ${amount} € par carte bancaire`);
// Logique de traitement Stripe/Adyen
}
refund(amount: number): void {
console.log(`Remboursement de ${amount} € sur la carte`);
}
saveCard(cardDetails: void): void {
console.log('Carte enregistrée de manière sécurisée');
}
authenticate(): void {
console.log('Authentication 3D Secure en cours...');
}
validateIBAN(): void {
throw new Error('IBAN non applicable pour les cartes bancaires');
}
sendSMS(): void {
console.log('SMS envoyé pour la validation');
}
}
2. Paiement PayPal #
class PayPalPayment implements Payment {
private email: string;
processPayment(amount: number): void {
console.log(`Paiement PayPal de ${amount} € via ${this.email}`);
}
refund(amount: number): void {
console.log(`Remboursement PayPal de ${amount} €`);
}
authorize3DS(): void {
throw new Error('PayPal ne gère pas la 3DS');
}
validateIBAN(): void {
throw new Error('PayPal n\'utilise pas d\'IBAN');
}
send3DS(): void {
throw new Error('PayPal envoie ses propres emails');
}
}
3. Paiement en Espèces #
class CashPayment implements Payment {
processPayment(amount: number): void {
console.log(`Paiement cash de ${amount} €`);
}
refund(amount: number): void {
console.log(`Remboursement cash de ${amount} €`);
}
// Pas besoin des autres méthodes !
}
Le Processeur de Paiements #
class PaymentProcessor {
process(payment: Payment, amount: number): void {
payment.processPayment(amount);
// Logique conditionnelle basée sur les capacités
if (this.isCardStorable(payment)) {
payment.saveCard();
}
if (this.isSecureAuth(payment)) {
payment.authenticate();
}
}
private isCardStorable(payment: Payment): payment is CardStorable {
return 'saveCard' in payment;
}
private isSecureAuth(payment: Payment): payment is SecureAuthenticable {
return 'authenticate' in payment;
}
}
Diagrammes UML #
Diagramme de Classes Complet #
classDiagram
class Payment {
<>
+processPayment(amount: number) void
+refund(amount: number) void
}
class CardStorable {
<>
+saveCard(cardDetails: void) void
}
class SecureAuthenticable {
<>
+authenticate() boolean
}
class Refundable {
<>
+refund(amount: number) void
}
class PaymentProcessor {
+process(payment: Payment, amount: number) void
}
class CreditCardPayment {
-cardNumber: string
-cvv: number
+processPayment(amount: number) void
+refund(amount: number) void
+saveCard() void
+authenticate() void
+validateIBAN() void
+sendSMS() void
}
class PayPalPayment {
-email: string
+processPayment(amount: number) void
+refund(amount: number) void
}
class CashPayment {
+processPayment(amount: number) void
+refund(amount: number) void
}
Payment <|-- CreditCardPayment
Payment <|-- PayPalPayment
Payment <|-- CashPayment
CardStorable <|.. CreditCardPayment
SecureAuthenticable <|.. CreditCardPayment
Refundable <|.. CreditCardPayment
Refundable <|.. PayPalPayment
PaymentProcessor ..> Payment : uses
Diagramme de Séquence : Processus de Paiement #
sequenceDiagram
participant Client
participant Processor as PaymentProcessor
participant Payment as CreditCardPayment
participant Gateway as PaymentGateway
Client->>Processor: process(payment, 100€)
Processor->>Payment: processPayment(100)
Payment->>Gateway: authorize(100€)
Gateway-->>Payment: success
Payment->>Payment: authenticate()
Payment-->>Processor: success
Processor->>Payment: saveCard()
Payment-->>Processor: saved
Processor-->>Client: payment confirmed
Utilisation Pratique #
// Initialisation
const processor = new PaymentProcessor();
// Paiement par carte
const cardPayment = new CreditCardPayment();
processor.process(cardPayment, 99.99);
// Paiement PayPal
const paypalPayment = new PayPalPayment();
processor.process(paypalPayment, 149.99);
// Paiement cash
const cashPayment = new CashPayment();
processor.process(cashPayment, 50.00);
Principes SOLID Appliqués #
Interface Segregation Principle (ISP) #
Chaque interface est spécialisée : CardStorable, SecureAuthenticable, Refundable. Les classes n’implémentent que ce dont elles ont besoin.
Dependency Inversion Principle (DIP) #
Le PaymentProcessor dépend de l’abstraction Payment, pas des implémentations concrètes.
Open/Closed Principle (OCP) #
Ajouter un nouveau moyen de paiement ne nécessite pas de modifier le code existant.
Comparaison : Avant/Après #
| Aspect | Sans Interfaces | Avec Interfaces |
|---|---|---|
| Couplage | Fort | Faible |
| Évolutivité | Difficile | Facile |
| Tests | Complexes | Simples (mocks) |
| Maintenance | Risquée | Sécurisée |
Conclusion #
Les interfaces sont essentielles pour créer des systèmes robustes et maintenables. En appliquant le principe ISP, nous obtenons :
- Code ciblé : Chaque classe implémente uniquement ce qu’elle utilise
- Flexibilité : Ajout facile de nouveaux moyens de paiement
- Testabilité : Injection de mocks pour les tests unitaires
- Architecture claire : Séparation des responsabilités