Complete Guide | Lesson 10: Normalization → Lesson 11: Polymorphism → Lesson 12: Architecture (Jun 11)
Your parent taught you to shake hands. You decided to fist bump instead. Same social contract (greet the person), different implementation. That's method overriding.
You answer the phone differently when it's your boss versus your best friend versus an unknown number. Same action (answer), different behavior based on context. That's polymorphism.
This is Lesson 11, and it maps to Yesod on the Tree of Life. Yesod means "Foundation." It's the sephira that connects everything above (the abstract concepts) to Malkuth below (the physical manifestation). Without Yesod, the ideas never become real. Without polymorphism, interfaces are just paperwork, inheritance is just copying, and design patterns are just theory. Polymorphism is the mechanism that makes all of object-oriented programming actually work at runtime.
Method Overriding: Same Name, Different Behavior
Overriding happens when a child class replaces a parent's method with its own implementation. The method signature (name, parameters, return type) stays the same. The behavior changes.
1CLASS Animal
2 VIRTUAL METHOD speak()
3 RETURN "..."
4 END METHOD
5END CLASS
6
7CLASS Dog EXTENDS Animal
8 OVERRIDE METHOD speak()
9 RETURN "Woof!"
10 END METHOD
11END CLASS
12
13CLASS Cat EXTENDS Animal
14 OVERRIDE METHOD speak()
15 RETURN "Meow."
16 END METHOD
17END CLASS
18
19CLASS Snake EXTENDS Animal
20 OVERRIDE METHOD speak()
21 RETURN "Hssssss."
22 END METHOD
23END CLASSAll four classes have a speak() method. The parent provides a default (silence). Each child replaces it with its own voice. The contract is honored (every Animal can speak). The implementation is individual.
Now the magic:
1animals = [new Dog(), new Cat(), new Snake()]
2
3FOR EACH animal IN animals
4 PRINT animal.speak()
5END FOR
6
7// Output:
8// Woof!
9// Meow.
10// Hssssss.One loop. One method call. Three different behaviors. The code doesn't ask "what type are you?" It just says "speak" and trusts each object to respond correctly. That's polymorphism in action.
The Keywords That Matter
Most languages use explicit keywords to mark overriding:
virtual (C#, C++): marks a parent method as "children may override me." Without this, the method is sealed, and overriding it would be an error or a hidden shadow.
override (C#, C++, Kotlin): explicitly declares "I am replacing my parent's behavior." This isn't just documentation. It's a safety net. If you misspell the method name, the compiler catches it because no parent method matches.
@Override (Java): an annotation that serves the same purpose. Optional but strongly recommended.
Python and JavaScript: no keywords needed. If you define a method with the same name in a child class, it overrides. No compiler checks. This is convenient and dangerous in equal measure.
Method Overloading: Same Name, Different Parameters
Overloading is NOT overriding. They sound similar but are completely different mechanisms.
Overloading means defining multiple methods with the same name but different parameter lists:
1CLASS Printer
2 METHOD print(text: String)
3 // Print a string
4 END METHOD
5
6 METHOD print(number: Integer)
7 // Print an integer
8 END METHOD
9
10 METHOD print(text: String, copies: Integer)
11 // Print a string multiple times
12 END METHOD
13END CLASS
14
15printer.print("Hello") // Calls version 1
16printer.print(42) // Calls version 2
17printer.print("Hello", 3) // Calls version 3Three methods, one name, three different parameter signatures. The compiler determines which one to call based on the arguments you pass. This is resolved at compile time, not runtime.
Why overloading exists: convenience. Without it, you'd need printString(), printInteger(), printStringWithCopies(). Overloading lets the method name stay intuitive while handling different input types.
Overriding vs Overloading
Overriding: Same method signature, different class (child replaces parent). Resolved at runtime. The object's actual type determines which version runs. Overloading: Same method name, different parameters, same class. Resolved at compile time. The argument types determine which version runs. Memory trick: Override = parent/child relationship (vertical). Overload = same-class variations (horizontal).
Under the Hood: The vtable
When you call animal.speak() and the runtime figures out whether to bark, meow, or hiss, something has to make that decision. That something is the vtable (virtual method table).
Every class that has virtual methods gets a vtable: a lookup table that maps method names to actual function pointers. Every object carries a hidden pointer to its class's vtable.
1Dog's vtable:
2 speak() → Dog.speak [function at memory address 0x4A20]
3 eat() → Animal.eat [inherited, address 0x3B10]
4
5Cat's vtable:
6 speak() → Cat.speak [function at memory address 0x5C30]
7 eat() → Animal.eat [inherited, address 0x3B10]When you call animal.speak():
- Follow the object's vtable pointer
- Look up
speak()in the table - Call the function at the address stored there
If the object is a Dog, the vtable points to Dog's speak. If it's a Cat, the vtable points to Cat's speak. The calling code doesn't know or care. It just follows the pointer.
This is why polymorphism works: the same method call on different objects routes to different implementations through the vtable. It's not magic. It's a pointer lookup in a table. But the design power it unlocks is extraordinary.
Abstract Classes vs Interfaces (The Final Clarification)
We covered interfaces in Lesson 6. We covered inheritance in Lesson 5. Now let's address the question that confuses everyone: when do you use an abstract class, and when do you use an interface?
Abstract Classes: Shared Code + Enforced Contract
An abstract class is a class that can't be instantiated directly. It exists only to be inherited. It can provide actual implementations (shared code) AND declare abstract methods that children must override.
1ABSTRACT CLASS DataMiner
2 // Shared implementation: every miner does this the same way
3 METHOD mine(path)
4 data = this.extract(path) // Abstract: each miner extracts differently
5 clean = this.transform(data) // Abstract: each miner transforms differently
6 this.load(clean) // Abstract: each miner loads differently
7 END METHOD
8
9 // Children MUST implement these:
10 ABSTRACT METHOD extract(path): RawData
11 ABSTRACT METHOD transform(data): CleanData
12 ABSTRACT METHOD load(data): void
13END CLASS
14
15CLASS CsvMiner EXTENDS DataMiner
16 OVERRIDE METHOD extract(path)
17 RETURN readCsvFile(path)
18 END METHOD
19 OVERRIDE METHOD transform(data)
20 RETURN parseCsvRows(data)
21 END METHOD
22 OVERRIDE METHOD load(data)
23 insertIntoDatabase(data)
24 END METHOD
25END CLASSThe parent defines the skeleton (mine → extract → transform → load). Children fill in the specific steps. This is the Template Method pattern from Lesson 8, and it's everywhere: ASP.NET request pipelines, React component lifecycles, Spring Boot controllers.
Interfaces: Capability Without Ancestry
An interface declares what an object can do without providing any implementation and without requiring shared ancestry.
1INTERFACE IExportable
2 METHOD exportToPdf(): Bytes
3 METHOD exportToCsv(): String
4END INTERFACE
5
6// Completely unrelated classes can implement the same interface:
7CLASS Invoice IMPLEMENTS IExportable
8 METHOD exportToPdf() // Invoice-specific PDF
9 METHOD exportToCsv() // Invoice-specific CSV
10END CLASS
11
12CLASS EmployeeReport IMPLEMENTS IExportable
13 METHOD exportToPdf() // Report-specific PDF
14 METHOD exportToCsv() // Report-specific CSV
15END CLASSInvoice and EmployeeReport have no common ancestor. They're not related by inheritance. But they both implement IExportable, so any code that needs "something exportable" can accept either one.
The Decision Matrix
| Question | Abstract Class | Interface |
|---|---|---|
| Need shared implementation? | Yes | No (traditionally) |
| Need multiple "parents"? | No (single inheritance) | Yes (implement many) |
| Related by ancestry? | Yes (is-a) | No (can-do) |
| Evolving API? | Easier (add methods with defaults) | Harder (breaks implementors) |
Runtime vs Compile-Time Polymorphism
Two kinds of polymorphism exist, and understanding the difference clarifies everything:
Compile-time polymorphism (static dispatch): The compiler resolves which method to call before the program runs. Method overloading and generics are compile-time. print(42) vs print("hello") is resolved by looking at the argument types during compilation.
Runtime polymorphism (dynamic dispatch): The program resolves which method to call while running. Method overriding via vtable lookup. animal.speak() calls Dog.speak or Cat.speak depending on the actual object type at runtime.
Static vs Dynamic Dispatch
Compile-time (static): Faster (no vtable lookup), but less flexible. The types must be known at compile time. Used for: overloading, generics, operator overloading. Runtime (dynamic): Slightly slower (vtable lookup), but infinitely flexible. The types can be anything that satisfies the contract. Used for: overriding, interface dispatch, plugin architectures. Rule of thumb: Use static dispatch for performance-critical paths. Use dynamic dispatch for flexibility and extensibility. Most of your code should use dynamic dispatch because developer time is more expensive than nanoseconds.
"In programming, as in everything else, to be in error is to be reborn." Alan Perlis, the first ACM Turing Award recipient, captured something essential: every override is a class being reborn with new behavior. Same contract. New life.