LESSON 1.5
Polymorphism and language contracts
Run the same notification operation on different channel implementations, with duck typing in JavaScript and generics in Java.
6 min read
A notification dispatcher starts with if channel is email and else if channel is SMS. A new channel means editing the dispatcher again. Polymorphism moves the channel-specific behavior behind a shared operation, so the caller can make the same request without enumerating implementations.
The problem
function deliver(kind, message) {
if (kind === "email") return `email: ${message}`;
if (kind === "sms") return `sms: ${message}`;
throw new Error("Unknown channel");
}The object boundary
Polymorphism means that the same operation can have different implementations selected by the receiver. The dispatcher below asks every channel to send a message. Each channel owns the details of that operation. Adding a push channel means implementing the contract and placing an instance in the collection, not inserting another branch into the loop. Construction still has to choose which objects participate; polymorphism does not remove that decision.
The shared contract must describe behavior as well as a method name. Here every channel accepts text and returns a delivery description. A channel that silently discards the text or returns a promise instead of a string would surprise this caller. Changing the contract to asynchronous delivery is possible, but all implementations and callers must agree on the change. Types can check the shape of a call; they cannot establish reliability or exactly-once delivery.
The Java example adds a type parameter to express the message type. Channel<String> sends strings, and the generic delivery method keeps the message and channel collection consistent. That is compile-time information, not a separate implementation of the method for every type. Java uses type erasure for most generic information. A JavaScript collection has no equivalent compiler check here, so the program relies on a documented protocol and tests.
In code
Both versions run the same small example. Java uses a single Example class; save it as Example.java.
When the boundary earns its place
Use polymorphism for a stable capability with multiple meaningful implementations. For two fixed cases in a small function, a switch can be clearer and cheaper to navigate. Avoid replacing a clear data transformation with a network of tiny classes just to remove an if statement. The goal is to keep a change local, not to ban branching. Test the shared contract against each implementation, then test the dispatcher once with a simple fake.
JavaScript and Java
JavaScript dispatches through properties and prototypes at runtime: duck typing allows an object literal with send to work alongside a class instance. Java uses nominal interfaces for these channel objects, and overload resolution is different from runtime overriding. Generics constrain which messages can reach a channel; they do not choose a channel for you. Familiar examples include Java Comparator implementations and JavaScript Array.prototype.sort callbacks, where the caller requests one operation and the supplied implementation decides how to perform it.
Try it
In-process Event Bus LLD
Identify the objects and state the rule each one owns before adding a pattern.