Complete Guide | Lesson 5: Inheritance → Lesson 6: Interfaces & Contracts → Lesson 7: SOLID (May 7)
Every wall outlet in your house makes the same promise: plug something in, and I'll give you power. A lamp, a blender, a phone charger, a vacuum cleaner. The outlet doesn't care. It promises 120 volts through a standardized socket, and anything that fits the plug can use it.
That's an interface. A promise of behavior without any specification of how that behavior is implemented.
This is Lesson 6, and it maps to Chesed on the Tree of Life. Chesed means "Mercy" or "Lovingkindness." It's the sephira of expansion, generosity, and giving without restriction. Chesed doesn't ask how you'll use the gift. It simply provides. An interface works the same way: it expands what objects can do without demanding how they do it. It gives power to any object that fulfills the contract.
What Is an Interface?
An interface defines what an object can do without specifying how it does it. It's a contract. A promise. A guarantee that says: "I can do these things. Ask me to do them. Don't worry about the details."
1INTERFACE IPayable
2 METHOD processPayment(amount: Decimal): Boolean
3 METHOD refund(transactionId: String): Boolean
4 METHOD getBalance(): Decimal
5END INTERFACEAny class that implements IPayable must provide these three methods. How it provides them is its own business:
1CLASS CreditCard IMPLEMENTS IPayable
2 METHOD processPayment(amount)
3 // Contact card network, authorize, settle
4 RETURN success
5 END METHOD
6
7 METHOD refund(transactionId)
8 // Reverse the charge through the card network
9 RETURN success
10 END METHOD
11
12 METHOD getBalance()
13 RETURN this.creditLimit - this.usedCredit
14 END METHOD
15END CLASS
16
17CLASS PayPal IMPLEMENTS IPayable
18 METHOD processPayment(amount)
19 // Authenticate with PayPal API, deduct from wallet
20 RETURN success
21 END METHOD
22
23 METHOD refund(transactionId)
24 // Credit back to PayPal wallet
25 RETURN success
26 END METHOD
27
28 METHOD getBalance()
29 RETURN this.walletBalance
30 END METHOD
31END CLASSCreditCard talks to a card network. PayPal talks to an API. Bitcoin would talk to a blockchain. Completely different implementations. Identical contract.
Polymorphism: Many Forms, One Interface
The word polymorphism comes from Greek: poly (many) + morphe (forms). It means treating different objects the same way through a shared interface.
Here's the magic. Your checkout system doesn't need to know whether it's dealing with a credit card, PayPal, or cryptocurrency:
1FUNCTION processCheckout(cart, paymentMethod: IPayable)
2 total = cart.calculateTotal()
3 success = paymentMethod.processPayment(total)
4 IF success THEN
5 PRINT "Payment processed!"
6 generateReceipt(cart, paymentMethod)
7 ELSE
8 PRINT "Payment failed. Try another method."
9 END IF
10END FUNCTION
11
12// Works with any IPayable implementation:
13processCheckout(myCart, new CreditCard("4111-1111-1111-1111"))
14processCheckout(myCart, new PayPal("[email protected]"))
15processCheckout(myCart, new Bitcoin("1A1zP1eP..."))One function. Three completely different payment systems. Zero changes needed when you add a fourth. That's polymorphism.
Killing the If-Else Chain
Without polymorphism, adding payment methods looks like this:
1// Without interfaces - grows with every new payment type
2FUNCTION processPayment(type, amount)
3 IF type == "credit_card" THEN
4 contactCardNetwork(amount)
5 ELSE IF type == "paypal" THEN
6 callPayPalAPI(amount)
7 ELSE IF type == "bitcoin" THEN
8 broadcastToBlockchain(amount)
9 ELSE IF type == "apple_pay" THEN
10 callAppleAPI(amount)
11 // ...this keeps growing forever
12 END IF
13END FUNCTIONEvery new payment method means modifying this function. Every modification risks breaking existing methods. This violates the Open/Closed Principle (which we'll cover in Lesson 7): open for extension, closed for modification.
With polymorphism, you never modify the checkout code. You just create a new class that implements IPayable and pass it in. The checkout doesn't know and doesn't care.
Before and After Polymorphism
Before: 47-line if/else chain that grows every time you add a feature. Every change risks breaking something. Testing requires hitting every branch. After: One method call that works for any implementation. Adding features means adding classes, not modifying existing code. Testing each implementation is independent. This is the difference between code that scales and code that collapses under its own weight.
Dependency Inversion: Depending on Promises, Not Details
Here's one of the most important principles in software design, and it comes directly from understanding interfaces:
High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
Without dependency inversion:
1CLASS CheckoutService
2 creditCard: CreditCard = new CreditCard()
3
4 METHOD checkout(cart)
5 this.creditCard.processPayment(cart.total)
6 END METHOD
7END CLASSThe CheckoutService is welded to CreditCard. Want PayPal? Rewrite the service. Want to test without charging real cards? Can't.
With dependency inversion:
1CLASS CheckoutService
2 paymentMethod: IPayable
3
4 CONSTRUCTOR(paymentMethod: IPayable)
5 this.paymentMethod = paymentMethod
6 END CONSTRUCTOR
7
8 METHOD checkout(cart)
9 this.paymentMethod.processPayment(cart.total)
10 END METHOD
11END CLASS
12
13// Inject any implementation:
14service = new CheckoutService(new CreditCard(...))
15service = new CheckoutService(new PayPal(...))
16service = new CheckoutService(new FakePayment()) // For testing!The CheckoutService depends on the interface, not the implementation. You can swap payment providers without touching the service. You can inject a fake payment object for testing that never charges real money. The service doesn't know the difference, and that's the point.
Duck Typing: If It Quacks Like a Duck
Not every language has a formal interface keyword. Python, JavaScript, and Ruby use what's called duck typing: "If it walks like a duck and quacks like a duck, it's a duck."
In duck-typed languages, you don't declare "I implement IPayable." You just provide the methods that the caller expects:
1// Python-style duck typing
2class CreditCard:
3 def process_payment(self, amount):
4 # process credit card
5 return True
6
7class PayPal:
8 def process_payment(self, amount):
9 # process PayPal
10 return True
11
12def checkout(payment_method, amount):
13 # Doesn't check the type. Just calls the method.
14 payment_method.process_payment(amount)If payment_method has a process_payment() method, it works. If it doesn't, it crashes at runtime. No compiler to catch the mistake.
Structural typing (TypeScript, Go) is a middle ground: the compiler checks that an object has the right shape (methods and properties) without requiring an explicit "implements" declaration.
Nominal typing (Java, C#) requires you to explicitly write class X implements Y. The compiler enforces the contract at compile time.
Typing Systems Compared
Duck typing (Python, JS, Ruby): No formal interfaces. If the method exists, it works. If not, runtime crash. Maximum flexibility, minimum safety. Structural typing (TypeScript, Go): The compiler checks the shape, not the declared type. If it has the right methods, it fits. Good balance. Nominal typing (Java, C#): You must explicitly declare which interfaces you implement. Maximum safety, slightly more verbose. Each has trade-offs. The concept of programming to an interface applies regardless of which system your language uses.
"Program to an interface, not an implementation." This advice from the Gang of Four's Design Patterns (1994) is arguably the single most impactful sentence in the history of software design. Every pattern, framework, and architectural principle that followed builds on this foundation.