Practice · SOLID · OCP · Card 1
What goes wrong when you add a third payment type?
The code below works for two payment methods. Adding a third should be additive. The way this is structured, it isn't.
The code
A service that processes a payment. Two methods supported today.
class ProcessPayment
def call(order, method)
case method
when :stripe
Stripe::Charge.create(amount: order.total_cents, source: order.stripe_token)
order.update!(paid_at: Time.current, payment_method: "stripe")
when :paypal
PayPal::Payment.create(amount: order.total_cents, payer: order.paypal_payer_id)
order.update!(paid_at: Time.current, payment_method: "paypal")
else
raise "Unknown payment method: #{method}"
end
end
end The question
You're asked to add Apple Pay. What's the OCP-flavored problem with how this code is shaped? And what's the shape of the fix?
Take a moment. Adding a new behavior should let you add code without changing existing code. What part of this design forces existing code to change every time the set of payment methods changes?
What's wrong
The case statement is the OCP smell. Every new payment method means editing this method, adding another when branch, re-testing the whole thing, and risking a regression in the existing branches. The class is not closed against new types.
It also tangles two responsibilities together: which gateway to call and how to mark the order paid. They both repeat per branch.
The OCP-friendly shape
Pull each gateway into its own class that responds to the same method. The dispatch becomes a lookup, not a case statement.
class StripeProcessor
def charge(order)
Stripe::Charge.create(amount: order.total_cents, source: order.stripe_token)
end
end
class PayPalProcessor
def charge(order)
PayPal::Payment.create(amount: order.total_cents, payer: order.paypal_payer_id)
end
end
class ProcessPayment
PROCESSORS = {
stripe: StripeProcessor.new,
paypal: PayPalProcessor.new,
}
def call(order, method)
processor = PROCESSORS.fetch(method) { raise "Unknown payment method: #{method}" }
processor.charge(order)
order.update!(paid_at: Time.current, payment_method: method.to_s)
end
end Now adding Apple Pay is: create ApplePayProcessor, register it in PROCESSORS. The orchestration code doesn\'t change. The existing processors don\'t change. Their tests don\'t change.
The registry pattern is the most common shape this takes in Rails. delegated_type is another shape (when each type also has its own model). adapter is a third (when you want to wrap an external API behind a stable internal interface).
Theory
For the full walkthrough of the registry pattern in real OSS, read SOLID · OCP · Registry (anchored in Gumroad's payment processor code), or Open/Closed in Practice — Gumroad payouts.