Interfaces in Object-Oriented Programming: SOLID Principle and Practical Example #
Introduction #
In this article, we will explore the Interface Segregation Principle (ISP) of SOLID principles through a concrete example of a payment system. We will see how interfaces allow us to create flexible, maintainable code that respects good design practices.
What is an Interface? #
An interface defines a contract that classes must respect. It specifies what to do without specifying how to do it. It is a fundamental concept of abstraction in OOP.
Advantages of interfaces #
- Decoupling: Reduction of dependencies between components
- Flexibility: Facilitates the addition of new implementations
- Testability: Enables dependency injection and mocks
- Polymorphism: One interface, multiple implementations
Case Study: Payment System #
Imagine an e-commerce system requiring multiple payment methods. Here is how to structure the code with interfaces.
Global Architecture - Initial Problem #
classDiagram
class Payment {
<<abstract>>
+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
}
Problem: This interface violates the ISP principle! Not all payment methods need all these features.
- PayPal doesn’t need
saveCard(),authorize3DS(),validateIBAN()orsendSMS() - Cash payment only needs
processPayment()andrefund() - Each class is forced to implement unnecessary methods
Implementation with Monolithic Interface #
1. Credit Card Payment #
class CreditCardPayment extends Payment {
private cardNumber: string;
private cvv: string;
processPayment(amount: number): void {
console.log('Credit card payment of ' + amount + '€');
}
refund(amount: number): void {
console.log('Refund of ' + amount + '€');
}
saveCard(): void {
console.log('Card saved');
}
authorize3DS(): void {
console.log('3D Secure authorization');
}
validateIBAN(): void {
// Doesn't make sense for a credit card!
throw new Error('Credit cards do not have IBAN');
}
sendSMS(): void {
console.log('SMS sent');
}
}
2. PayPal Payment #
class PayPalPayment extends Payment {
private email: string;
processPayment(amount: number): void {
console.log('PayPal payment of ' + amount + '€');
}
refund(amount: number): void {
console.log('PayPal refund of ' + amount + '€');
}
saveCard(): void {
// PayPal does not manage cards!
throw new Error('PayPal does not manage cards');
}
authorize3DS(): void {
// PayPal doesn't use 3DS!
throw new Error('PayPal does not use 3DS');
}
validateIBAN(): void {
// PayPal doesn't use IBAN!
throw new Error('PayPal does not use IBAN');
}
sendSMS(): void {
// PayPal sends emails, not SMS!
throw new Error('PayPal sends emails');
}
}
3. Cash Payment #
class CashPayment extends Payment {
processPayment(amount: number): void {
console.log('Cash payment of ' + amount + '€');
}
refund(amount: number): void {
console.log('Cash refund of ' + amount + '€');
}
saveCard(): void {
// Cash doesn't have a card!
throw new Error('Cash does not have a card');
}
authorize3DS(): void {
// No 3DS for cash!
throw new Error('No 3DS for cash');
}
validateIBAN(): void {
// No IBAN for cash!
throw new Error('No IBAN for cash');
}
sendSMS(): void {
// No SMS for cash!
throw new Error('No SMS for cash');
}
}
Consequences of Poor Design #
This code presents several serious problems:
- LSP violations: Subclasses throw exceptions for inherited methods
- Dead code: Many unused methods that clutter the code
- Difficult maintenance: Adding a new method impacts all classes
- Complex tests: Need to test methods that shouldn’t exist
- Difficult understanding: The API is misleading about what each class can actually do
Problem: This interface violates the ISP principle! Not all payment methods need all these features.
Solution: Interface Segregation #
// Base interface - common features
interface Payment {
processPayment(amount: number): void;
refund(amount: number): void;
}
// Interface for card storage
interface CardStorable {
saveCard(cardDetails: void): void;
}
// Interface for 3D Secure authentication
interface SecureAuthenticable {
authenticate(): boolean;
}
// Interface for IBAN validation
interface Refundable {
refund(amount: number): void;
}
Implementation of Concrete Classes #
1. Credit Card Payment #
class CreditCardPayment implements Payment, CardStorable, SecureAuthenticable {
private cardNumber: string;
private cvv: number;
processPayment(amount: number): void {
console.log(`Payment of ${amount} € by credit card`);
// Stripe/Adyen processing logic
}
refund(amount: number): void {
console.log(`Refund of ${amount} € to the card`);
}
saveCard(cardDetails: void): void {
console.log('Card saved securely');
}
authenticate(): void {
console.log('3D Secure authentication in progress...');
}
validateIBAN(): void {
throw new Error('IBAN not applicable for credit cards');
}
sendSMS(): void {
console.log('SMS sent for validation');
}
}
2. PayPal Payment #
class PayPalPayment implements Payment {
private email: string;
processPayment(amount: number): void {
console.log(`PayPal payment of ${amount} € via ${this.email}`);
}
refund(amount: number): void {
console.log(`PayPal refund of ${amount} €`);
}
authorize3DS(): void {
throw new Error('PayPal does not manage 3DS');
}
validateIBAN(): void {
throw new Error('PayPal does not use IBAN');
}
send3DS(): void {
throw new Error('PayPal sends its own emails');
}
}
3. Cash Payment #
class CashPayment implements Payment {
processPayment(amount: number): void {
console.log(`Cash payment of ${amount} €`);
}
refund(amount: number): void {
console.log(`Cash refund of ${amount} €`);
}
// No need for other methods!
}
The Payment Processor #
class PaymentProcessor {
process(payment: Payment, amount: number): void {
payment.processPayment(amount);
// Conditional logic based on capabilities
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;
}
}
UML Diagrams #
Complete Class Diagram #
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
Sequence Diagram: Payment Process #
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
Practical Usage #
// Initialization
const processor = new PaymentProcessor();
// Credit card payment
const cardPayment = new CreditCardPayment();
processor.process(cardPayment, 99.99);
// PayPal payment
const paypalPayment = new PayPalPayment();
processor.process(paypalPayment, 149.99);
// Cash payment
const cashPayment = new CashPayment();
processor.process(cashPayment, 50.00);
SOLID Principles Applied #
Interface Segregation Principle (ISP) #
Each interface is specialized: CardStorable, SecureAuthenticable, Refundable. Classes only implement what they need.
Dependency Inversion Principle (DIP) #
The PaymentProcessor depends on the Payment abstraction, not on concrete implementations.
Open/Closed Principle (OCP) #
Adding a new payment method does not require modifying existing code.
Comparison: Before/After #
| Aspect | Without Interfaces | With Interfaces |
|---|---|---|
| Coupling | Strong | Weak |
| Evolvability | Difficult | Easy |
| Tests | Complex | Simple (mocks) |
| Maintenance | Risky | Secure |
Conclusion #
Interfaces are essential for creating robust and maintainable systems. By applying the ISP principle, we obtain:
- Targeted code: Each class implements only what it uses
- Flexibility: Easy addition of new payment methods
- Testability: Injection of mocks for unit tests
- Clear architecture: Separation of concerns