Advanced Polymorphism: Double Dispatch and Visitor Pattern
The Limitation of Single Dispatch
Standard polymorphism in object-oriented languages uses single dispatch: the method executed depends on the runtime type of one object (the receiver). However, some problems require double dispatch, where the method depends on the types of two objects.
Example Problem: In a game, different collision behaviors occur based on both objects involved. A Spaceship colliding with an Asteroid is different from a Spaceship colliding with another Spaceship. Single dispatch cannot elegantly handle this without type checking.
Double Dispatch Solution
+ collideWithSpaceship(s: Spaceship)
+ collideWithAsteroid(a: Asteroid)
+ collideWithSpaceship(s: Spaceship)
+ collideWithAsteroid(a: Asteroid)
+ collideWithSpaceship(s: Spaceship)
+ collideWithAsteroid(a: Asteroid)
other.collideWithSpaceship(this) // First dispatch on 'other' type
Asteroid.collideWithSpaceship(spaceship):
// Handle Spaceship-Asteroid collision // Second dispatch
First dispatch: collideWith is called on the first object, dispatching based on its type. Second dispatch: That method calls back to the other object with a type-specific method (e.g., collideWithSpaceship), dispatching based on the second object's type. This achieves behavior selection based on both types without type checking.
Visitor Pattern: Structured Double Dispatch
The Visitor Pattern formalizes double dispatch for adding operations to a stable class hierarchy without modifying the classes. It is useful when you have a fixed set of types but frequently add new operations.
+ visitMagazine(mag: Magazine)
+ visitDVD(dvd: DVD)
Use case: Library system needs to export items in different formats (JSON, XML, PDF). Instead of adding exportToJSON(), exportToXML(), exportToPDF() methods to each item class, create JSONExportVisitor, XMLExportVisitor, PDFExportVisitor implementations. Each item's accept method calls visitor.visitBook(this), enabling type-specific export logic in the visitor.
When to Use Visitor vs Standard Polymorphism
Use standard polymorphism when: Operations are core to the object's responsibility and rarely change. For example, calculateArea() for shapes is intrinsic and stable.
Use Visitor Pattern when: First, you have a stable class hierarchy that changes infrequently. Second, you need to add new operations frequently without modifying existing classes. Third, operations are external concerns (exporting, rendering, validation) rather than core behavior. However, if the class hierarchy changes often, Visitor becomes cumbersome because adding a new type requires updating all visitors.