Introduction
After exploring the Single Responsibility Principle, let's dive into the "O" in SOLID - the Open/Closed Principle (OCP). This principle states:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
At first glance, this might seem contradictory. How can something be open to extension yet closed for modification? Let's explore this concept through a practical example.
The Initial Problem
Consider a payment processing system for an e-commerce platform:
class PaymentProcessor
def process_payment(order)
# Hard-coded to only handle credit card payments
if order.total > 0
CreditCardPayment.charge(
amount: order.total,
card_number: order.credit_card_number,
expiry: order.card_expiry,
cvv: order.cvv
)
end
end
end
While this code works for credit card payments, it's not open for extension. If we want to add PayPal, crypto, or bank transfer payments, we'd need to modify the existing code. This violates the Open/Closed Principle.
Applying the Open/Closed Principle
Step 1: Dependency Injection
First, let's improve the design using dependency injection:
class PaymentProcessor
def process_payment(order, payment_method = CreditCardPayment.new)
payment_method.process(order) if order.total > 0
end
end
This simple change brings significant flexibility:
- Existing code continues to work (backwards compatibility)
- Credit card payment remains the default
- We can now inject different payment methods
Step 2: Creating a Common Interface
Let's create a payment interface that all payment methods must implement:
class PaymentMethod
def process(order)
raise NotImplementedError
end
end
class CreditCardPayment < PaymentMethod
def process(order)
# Process credit card payment
charge(
amount: order.total,
card_number: order.credit_card_number,
expiry: order.card_expiry,
cvv: order.cvv
)
end
private
def charge(amount:, card_number:, expiry:, cvv:)
# Implementation for credit card processing
end
end
class PayPalPayment < PaymentMethod
def process(order)
# Process PayPal payment
initiate_paypal_transaction(
amount: order.total,
email: order.paypal_email
)
end
private
def initiate_paypal_transaction(amount:, email:)
# Implementation for PayPal processing
end
end
class CryptoPayment < PaymentMethod
def process(order)
# Process cryptocurrency payment
create_crypto_transaction(
amount: order.total,
wallet_address: order.crypto_wallet
)
end
private
def create_crypto_transaction(amount:, wallet_address:)
# Implementation for crypto processing
end
end
Using the Improved Design
Now we can easily add new payment methods:
# Process with different payment methods
processor = PaymentProcessor.new
# Credit Card Payment (default)
processor.process_payment(order)
# PayPal Payment
processor.process_payment(order, PayPalPayment.new)
# Crypto Payment
processor.process_payment(order, CryptoPayment.new)
Real-World Extension Example
Let's say we need to add support for Apple Pay. With our new design, we simply create a new class:
class ApplePayPayment < PaymentMethod
def process(order)
verify_device_token(order.device_token)
process_apple_pay_transaction(
amount: order.total,
token: order.payment_token
)
end
private
def verify_device_token(token)
# Verify Apple Pay device token
end
def process_apple_pay_transaction(amount:, token:)
# Process Apple Pay transaction
end
end
# Use the new payment method
processor.process_payment(order, ApplePayPayment.new)
Benefits
- Flexibility: Easy to add new payment methods without changing existing code
- Reduced Risk: No need to modify working payment processing code
- Better Testing: Each payment method can be tested independently
- Maintainability: Clear separation between payment methods
- Code Reuse: Common interface ensures consistent implementation
Key Principle
As stated in the literature:
Design our modules, classes and functions in a way that when a new functionality is needed, we should not modify our existing code but rather write new code that will be used by existing code.
Our refactored payment system achieves this by:
- Defining a common interface for all payment methods
- Allowing new payment methods to be added without modifying the processor
- Maintaining backward compatibility with existing implementations
Conclusion
The Open/Closed Principle helps us create more flexible and maintainable code by:
- Using dependency injection to remove hard-coded dependencies
- Creating common interfaces through base classes
- Allowing for extension through inheritance or composition
In our payment processing example, we can now add support for any new payment method without touching the core payment processing code. This makes our system more robust and easier to maintain as requirements evolve.