5 Polymorphism
A practical guide to using polymorphism in C#, including dynamic dispatch, type relationships, substitutability, abstraction-based design, and composition.
: one abstraction, many implementations
allows different concrete types to be used through a shared abstraction. The caller uses a common operation, while the object supplies specialized behavior.
For example, a drawing application can process circles and rectangles as Shape objects. A method that calculates total area can call Area() on every Shape without knowing whether the current object is a Circle or a Rectangle. Each object provides its own implementation.
This gives the central pattern:
Uniform use: client code follows one common protocol.
Specialized behavior: each concrete type performs the operation in its own way.
Reduced coupling: the caller does not need to know implementation details.
A useful abstraction should represent a meaningful capability shared by its possible implementations, not merely provide a convenient place to collect unrelated methods.
Takeaway: separates what a caller needs to do from how each concrete type does it.
and
occurs when a call to a virtual or abstract operation is resolved at run time. Consider a variable declared as Shape that refers to a Circle object. The declared type makes Shape operations available, but the run-time type Circle determines that Circle's Area() implementation executes.
This differs from method overloading:
Overloading selects between methods with different parameter lists using compile-time information, such as the declared argument type.
supplies a derived implementation for an already-selected virtual or abstract operation.
chooses the most-derived override according to the object's run-time type.
Member hiding creates different behavior. If a derived class declares a method with new instead of override, a call can depend on the variable's declared type. With override, a call through a base-class reference reaches the derived implementation.
Takeaway: The declared type controls what the caller can request; the run-time type controls which override performs the request.
Type relationships and contracts
Type relationships determine how a value can participate in .
A subtype relationship lets a derived class be used where its base class is expected.
An interface relationship means that a class promises to provide the operations specified by an interface.
A structural relationship accepts a value because it has the required operations, even without explicit inheritance from a named type.
For example, an IChargeable interface can declare Charge(). Phone and Battery can both implement the interface, and a ChargeEverything method can process a collection of IChargeable values. The method depends on the public contract, not on either concrete class.
In languages that support , code may attempt an operation and accept an object if the object supplies the required behavior. This is more flexible about declared type relationships, but it still depends on a reliable behavioral expectation.
Takeaway: Inheritance, interfaces, and structural techniques can all support , but they express different kinds of relationships.
and behavioral contracts
is the condition that makes a polymorphic relationship safe. If a function accepts a Shape, every valid Shape should honor the behavior promised by that abstraction. A new subtype should not unexpectedly reject valid inputs, weaken required results, or violate documented guarantees.
The emphasizes that a subtype must preserve behavior, not just inherit names. Check a proposed subtype by asking:
Can it be passed wherever the supertype is expected?
Does it honor the supertype's preconditions and postconditions?
Does it preserve important invariants?
Does it avoid stronger requirements or weaker results that surprise callers?
For example, a ReadOnlyFile should not implement an interface named IWritableFile if callers are entitled to expect that Write() succeeds. The apparent type relationship would misrepresent the behavior. A smaller interface or a different abstraction would be safer.
Takeaway: A subtype is valid when it can keep the promises of the abstraction in every context where the abstraction is used.
Abstractions, interfaces, and
means that client code depends on capabilities rather than concrete implementation classes. A checkout operation can accept an IPaymentProcessor and call Process(order), allowing a production processor, a test processor, or another compatible implementation to be supplied without changing the checkout logic.
This design provides:
Changeability: implementations can be replaced without rewriting clients.
Testability: tests can provide fake or mock implementations.
Extensibility: new implementations can be added without modifying the caller.
Separation of concerns: the caller specifies what it needs, while the implementation determines how to provide it.
A useful abstraction should be small, meaningful, and behaviorally reliable. Several focused interfaces are often easier to implement and substitute than one interface containing many unrelated operations.
provides another way to assemble polymorphic behavior. A Report can contain an IFormatter and delegate rendering to it. The formatter can then be an HTML formatter, a plain-text formatter, or a test formatter without requiring a separate Report subclass for each output style.
Prefer when the relationship is “has a” rather than “is a.” Use inheritance when the subtype genuinely satisfies the base abstraction's behavioral contract.
Takeaway: Abstractions make dependencies replaceable, while can provide flexible behavior without deep inheritance hierarchies.
Design guidelines and final checklist
Use when several types share a meaningful operation, callers should treat them uniformly, behavior varies by object type, new implementations may be added, or dependencies need to be replaceable in tests and configurations.
Before introducing a hierarchy or interface, verify that the relationship expresses a real contract. Avoid creating inheritance solely to reuse a few helper methods. Avoid interfaces with many unrelated responsibilities. Confirm that every implementation can satisfy the same expectations without surprising callers.
A compact design review can ask:
What common capability does the abstraction represent?
Which operations should clients be allowed to use?
Does each implementation honor the same behavioral contract?
Would express the design more clearly than inheritance?
Can a new implementation be added without changing existing client logic?
Final takeaway: Effective combines a clear abstraction, safe , appropriate dispatch, and replaceable implementations. It is valuable when it expresses a genuine common contract, not when it merely adds layers.