Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartSwiggyRazorpayPhonePe

Polymorphism - Compile-Time (Overloading) vs Runtime (Overriding)

Choose your preparation mode3 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Steps
setup

Define base class Shape

The base class Shape is defined with a draw() method that prints a generic message.

💡 This sets up the parent class that other shapes will inherit from, establishing the base behavior.
Line:class Shape: def draw(self): print("Drawing a generic shape")
💡 Shape class provides a default draw() method to be overridden by subclasses.
📊
Polymorphism - Compile-Time (Overloading) vs Runtime (Overriding) - Watch the Algorithm Execute, Step by Step
Watching each step reveals how polymorphism works internally, clarifying the difference between compile-time overloading and runtime overriding.
Step 1/10
·Active fillAnswer cell
Defines base class with a concrete method to be overridden.
Shape
+draw()
Subclass overrides base class method to customize behavior.
Shape
+draw()
Circle
+draw()
Circle Shape
Multiple subclasses override the same base method.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Objects instantiated from classes to demonstrate polymorphism.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Loop setup to traverse polymorphic objects.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Runtime method dispatch selects subclass method.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Runtime dispatch calls correct subclass method.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Base class method called when no override exists.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Iteration complete, polymorphic calls demonstrated.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape
Final understanding of runtime polymorphism via overriding.
Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle ShapeRectangle Shape

Key Takeaways

Runtime polymorphism allows method calls to invoke the correct subclass implementation dynamically.

This dynamic dispatch is not obvious from reading code alone but is clear when watching method calls resolve at runtime.

Overriding replaces base class methods in subclasses, enabling flexible behavior customization.

Seeing the overridden methods highlighted helps understand how subclasses change behavior.

The base class method is called only if no subclass override exists, showing fallback behavior.

This clarifies that polymorphism does not always mean subclass methods are called; base methods remain accessible.

Practice

(1/5)
1. Trace the sequence of events when a client calls a method on a subclass instance that violates the Liskov Substitution Principle by strengthening a postcondition. What happens step-by-step?
easy
A. The client receives a result that meets the superclass contract, so no issues arise.
B. The subclass method returns a stricter result than expected, potentially causing client failures.
C. The client silently ignores the stricter postcondition, so behavior is unaffected.
D. The subclass method throws an exception due to the strengthened postcondition.

Solution

  1. Step 1: Recall LSP postcondition rule

    Subclasses must not strengthen postconditions; they can only maintain or weaken them.
  2. Step 2: Trace client call

    The client expects results conforming to the superclass contract. If subclass returns stricter results, some clients expecting broader results may fail.
  3. Step 3: Analyze options

    The subclass method returns a stricter result than expected, potentially causing client failures. correctly identifies potential client failures due to stricter postconditions. The client receives a result that meets the superclass contract, so no issues arise. is false because stricter postconditions can break clients. The client silently ignores the stricter postcondition, so behavior is unaffected. is incorrect; clients cannot ignore contract violations silently. The subclass method throws an exception due to the strengthened postcondition. is not guaranteed; exceptions are not implied by postcondition strengthening.
  4. Final Answer:

    Option B -> Option B
  5. Quick Check:

    Strengthening postconditions risks breaking client expectations.
Hint: Strengthening postconditions breaks client assumptions and causes failures.
Common Mistakes:
  • Assuming stricter postconditions are safe
  • Believing clients ignore contract violations
  • Confusing exceptions with contract violations
2. You need to ensure that a class has only one instance throughout the application lifecycle, and this instance must be lazily initialized in a thread-safe manner without incurring synchronization overhead on every access. Which design approach best fits this requirement?
easy
A. Use a simple static variable without synchronization and rely on the language's memory model.
B. Use a synchronized method that creates the instance on first call, locking every time.
C. Create the instance eagerly at class loading time to avoid synchronization issues.
D. Implement double-checked locking with a volatile instance variable to minimize synchronization overhead.

Solution

  1. Step 1: Understand the problem constraints

    The instance must be lazily initialized and thread-safe, but synchronization overhead should be minimized.
  2. Step 2: Evaluate each approach

    Synchronized method locks on every call, causing overhead. Eager initialization is thread-safe but not lazy. Unsynchronized static variable risks multiple instances in multithreaded contexts. Double-checked locking with volatile ensures lazy init, thread safety, and minimal locking.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Double-checked locking balances thread safety and performance [OK]
Hint: Double-checked locking minimizes synchronization overhead [OK]
Common Mistakes:
  • Assuming synchronized method is efficient enough
  • Believing eager initialization is always best
  • Ignoring volatile keyword necessity
3. What is a common trade-off or limitation when applying Dependency Inversion Principle (DIP) with heavy use of Dependency Injection frameworks?
medium
A. It always improves runtime performance by reducing object creation overhead.
B. It eliminates the need for interfaces or abstractions entirely.
C. It can increase complexity and reduce code readability due to indirect dependencies and configuration.
D. It guarantees compile-time safety without any runtime errors.

Solution

  1. Step 1: Understand DIP and DI trade-offs

    While DIP and DI improve modularity, heavy use of DI frameworks can add complexity and obscure dependencies.
  2. Step 2: Analyze options

    It can increase complexity and reduce code readability due to indirect dependencies and configuration. correctly identifies increased complexity and reduced readability as a trade-off. It always improves runtime performance by reducing object creation overhead. is false; DI can add runtime overhead. It eliminates the need for interfaces or abstractions entirely. is wrong; DIP requires abstractions. It guarantees compile-time safety without any runtime errors. is incorrect; runtime errors can still occur due to misconfiguration.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    DI frameworks improve flexibility but can complicate understanding and debugging.
Hint: DI improves modularity but can complicate code and configs.
Common Mistakes:
  • Assuming DI always improves performance
  • Believing DIP removes need for interfaces
  • Thinking DI guarantees no runtime errors
4. Identify the bug in the following singleton implementation that aims to use lazy initialization with double-checked locking in Java-like pseudocode:
class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
What is the subtle bug that can cause thread-safety issues?
medium
A. The synchronized block is too large, causing unnecessary locking overhead.
B. The instance variable is not declared volatile, risking instruction reordering.
C. The first null check outside synchronized block is redundant and should be removed.
D. The constructor is not private, allowing multiple instances externally.

Solution

  1. Step 1: Analyze double-checked locking correctness

    Without volatile, the instance reference may be visible before full construction due to instruction reordering.
  2. Step 2: Check other options

    Synchronized block size is minimal and correct; first null check is necessary for performance; constructor privacy is unrelated to this snippet.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Missing volatile causes subtle thread-safety bugs [OK]
Hint: Volatile prevents instruction reordering in double-checked locking [OK]
Common Mistakes:
  • Ignoring volatile keyword necessity
  • Thinking synchronized block size is the bug
  • Assuming constructor privacy is the main issue here
5. Suppose you want to extend the observer pattern to allow observers to receive multiple notifications for the same event type without unsubscribing (i.e., observers can be registered multiple times for the same event). Which modification to the thread-safe observer pattern implementation below correctly supports this requirement without breaking thread safety or notification correctness? Options: A) Change the observers data structure from a set to a list per event type and allow duplicates. B) Keep using a set but add a counter for each observer to track multiple subscriptions. C) Use a dictionary mapping observers to their subscription counts per event type. D) Use a queue per event type to enqueue notifications and process them asynchronously.
hard
A. Keep using a set but add a counter for each observer to track multiple subscriptions.
B. Change the observers data structure from a set to a list per event type and allow duplicates.
C. Use a dictionary mapping observers to their subscription counts per event type.
D. Use a queue per event type to enqueue notifications and process them asynchronously.

Solution

  1. Step 1: Understand the requirement

    Observers can subscribe multiple times to the same event type and should receive multiple notifications accordingly.
  2. Step 2: Evaluate data structure changes

    Using a set alone disallows duplicates. A list allows duplicates but is not thread-safe and inefficient for removals. A dictionary mapping observers to counts tracks multiple subscriptions safely.
  3. Step 3: Choose the best approach

    Adding a counter per observer in the set (Keep using a set but add a counter for each observer to track multiple subscriptions.) or using a dictionary (Use a dictionary mapping observers to their subscription counts per event type.) can work, but adding a counter per observer in the set is simpler and keeps thread safety with minimal changes.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Counting subscriptions per observer preserves multiple notifications safely [OK]
Hint: Track subscription counts to allow multiple notifications [OK]
Common Mistakes:
  • Using list causes concurrency issues and inefficient removals