Skip to main content

Polymorphism

Handle object type variations through polymorphic types rather than conditional logic

TL;DR

The Polymorphism pattern encourages you to handle object type variations through inheritance and method overriding rather than through conditional logic. Instead of checking object types with if/else statements, you define a common interface that different types implement, allowing the correct behavior to be selected automatically based on the actual object type.

Learning Objectives

  • Understand how polymorphism eliminates conditional type-checking
  • Learn when to create polymorphic hierarchies
  • Apply polymorphism to handle behavior variations elegantly
  • Recognize and refactor code that uses conditional logic instead of polymorphism
  • Balance polymorphism with simplicity and maintainability

Motivating Scenario

You're processing payments and have multiple payment methods: credit card, bank transfer, and digital wallet. You could write a PaymentProcessor with a massive if/else block checking the payment type. Or you could create a Payment interface with CreditCardPayment, BankTransferPayment, and DigitalWalletPayment implementations. Now each payment type knows how to process itself, and you can add new types without modifying existing code.

Core Concepts

Polymorphism is the ability of different objects to respond to the same message (method call) in their own way. The GRASP Polymorphism pattern advocates using polymorphic types and method overriding to handle variations in behavior, rather than embedding type-checking logic throughout your code.

Replacing conditional logic with polymorphism provides benefits:

  1. Extensibility: Add new types without modifying existing code (Open/Closed Principle)
  2. Maintainability: Each type encapsulates its own behavior
  3. Clarity: No scattered conditional logic; the code reads more naturally
  4. Testability: Each polymorphic type can be tested independently
  5. Type Safety: Compile-time checking in statically-typed languages

Without polymorphism, code filled with type checks becomes brittle. Adding a new type requires finding and modifying every if/else block. With polymorphism, new types are simply new implementations.

Polymorphism: Replacing Conditionals

Practical Example

Let's see how to replace conditional logic with polymorphism:

polymorphism_example.py
from abc import ABC, abstractmethod

# WITHOUT POLYMORPHISM (avoid)
class BadPaymentProcessor:
def process_payment(self, payment_data: dict) -> bool:
if payment_data["type"] == "credit_card":
# Process credit card
print(f"Charging {payment_data['card_number']}")
return True
elif payment_data["type"] == "bank_transfer":
# Process bank transfer
print(f"Transferring from {payment_data['account']}")
return True
elif payment_data["type"] == "digital_wallet":
# Process digital wallet
print(f"Using {payment_data['wallet_id']}")
return True
return False

# WITH POLYMORPHISM (good design)
class Payment(ABC):
"""Base class defining payment interface"""
@abstractmethod
def process(self) -> bool:
pass

class CreditCardPayment(Payment):
def __init__(self, card_number: str, amount: float):
self.card_number = card_number
self.amount = amount

def process(self) -> bool:
print(f"Charging card {self.card_number}: ${self.amount}")
return True

class BankTransferPayment(Payment):
def __init__(self, account: str, amount: float):
self.account = account
self.amount = amount

def process(self) -> bool:
print(f"Transferring from {self.account}: ${self.amount}")
return True

class DigitalWalletPayment(Payment):
def __init__(self, wallet_id: str, amount: float):
self.wallet_id = wallet_id
self.amount = amount

def process(self) -> bool:
print(f"Using wallet {self.wallet_id}: ${self.amount}")
return True

class PaymentProcessor:
"""Works with any Payment implementation"""
def process_payment(self, payment: Payment) -> bool:
return payment.process()

# Usage
processor = PaymentProcessor()

# No type checking needed!
cc_payment = CreditCardPayment("4111-1111-1111-1111", 99.99)
processor.process_payment(cc_payment)

transfer = BankTransferPayment("ACC-12345", 150.00)
processor.process_payment(transfer)

wallet = DigitalWalletPayment("wallet-567", 49.99)
processor.process_payment(wallet)

When to Use / When Not to Use

Use
  1. When you have multiple related types that behave differently
  2. When adding new types should not require modifying existing code
  3. When the same operation has different implementations
  4. When you want to avoid scattered type-checking logic
  5. When behavior varies based on object type
Avoid
  1. For simple single-type objects with no variation
  2. When polymorphic hierarchy requires too many levels
  3. When different behavior is based on state, not type
  4. When it adds unnecessary complexity to simple cases
  5. When simple conditionals are clearer than polymorphism

Patterns and Pitfalls

Polymorphism Implementation

Define clear contracts: Create interfaces or abstract classes that define the contract all implementations must follow. Each type knows how to fulfill that contract.

One responsibility per implementation: Each polymorphic type should encapsulate its own variation in behavior. Don't have one massive implementation that handles multiple cases internally.

Replace type checking gradually: Look for if/else blocks that check object type or type-like properties. Replace with polymorphism incrementally.

Deep inheritance hierarchies: Limit polymorphic hierarchies to 2-3 levels. Deep hierarchies become hard to understand and maintain.

Mixing state and type: If different behavior depends on internal state, not type, use Strategy pattern or state-based conditionals instead of type-based polymorphism.

Over-polymorphizing: Not every variation needs polymorphism. Simple, predictable variations might be fine as conditional logic or configuration.

Design Review Checklist

  • Is there a clear interface or base type defining the contract?
  • Does each concrete type implement this contract in its own way?
  • Are there if/else blocks checking object type that could be replaced?
  • Can new types be added without modifying existing code?
  • Is the inheritance hierarchy shallow (2-3 levels max)?
  • Are polymorphic types used where they appear, not passed through intermediate layers unnecessarily?

Self-Check

  1. What's the main benefit of polymorphism? It allows you to add new types and behaviors without modifying existing code, making systems more extensible and maintainable.

  2. When should you use polymorphism instead of conditionals? When you have multiple types that should behave differently and you expect to add new types in the future.

  3. What's the danger of deep inheritance hierarchies? They become hard to understand, maintain, and modify. Keep polymorphic hierarchies shallow (typically 2-3 levels).

info

One Takeaway: Replace type-checking conditionals with polymorphic types. Let each type encapsulate its own behavior rather than having one controller check types and branch accordingly.

Next Steps

References

  1. GRASP (Object-Oriented Design) - Wikipedia ↗️
  2. Applying UML and Patterns by Craig Larman ↗️