Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartRazorpaySwiggyPhonePe

Strategy Pattern - Replace Conditionals with Polymorphism

Choose your preparation mode3 modes available
▶
Steps
setup

Define PaymentStrategy Interface

The abstract PaymentStrategy interface is defined with an abstract method pay(amount). This sets the contract for all payment strategies.

💡 Defining an interface ensures all payment methods implement the same method, enabling polymorphism.
Line:class PaymentStrategy(ABC): @abstractmethod def pay(self, amount): pass
💡 All payment strategies will share a common interface, allowing interchangeable use.
📊
Strategy Pattern - Replace Conditionals with Polymorphism - Watch the Algorithm Execute, Step by Step
Watching each step reveals how polymorphism replaces conditional logic, making the code more flexible and extensible.
Step 1/10
·Active fill★Answer cell
Defines the Strategy interface with an abstract method for payment.
«abstract»PaymentStrategy
+pay()
Concrete strategy implements the Strategy interface.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
CreditCardStrategy ▷ PaymentStrategy
Another concrete strategy implementing the Strategy interface.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategy
Factory pattern encapsulates strategy selection logic.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategy
Dependency injection of strategy into context class.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
PaymentProcessor
−strategy: PaymentStrategy
+__init__()
+set_strategy()
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategyPaymentProcessor ◇ PaymentStrategy (1:1)
Context delegates behavior to current strategy.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
PaymentProcessor
−strategy: PaymentStrategy
+__init__()
+set_strategy()
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategyPaymentProcessor ◇ PaymentStrategy (1:1)
Context updates its strategy reference to a new concrete strategy.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
PaymentProcessor
−strategy: PaymentStrategy
+__init__()
+set_strategy()
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategyPaymentProcessor ◇ PaymentStrategy (1:1)
Context delegates to the updated strategy's method.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
PaymentProcessor
−strategy: PaymentStrategy
+__init__()
+set_strategy()
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategyPaymentProcessor ◇ PaymentStrategy (1:1)
Shows the overall design where polymorphism replaces conditionals.
«abstract»PaymentStrategy
+pay()
CreditCardStrategy
+pay()
UPIStrategy
+pay()
PaymentStrategyFactory
+get_strategy()
PaymentProcessor
−strategy: PaymentStrategy
+__init__()
+set_strategy()
+pay()
CreditCardStrategy ▷ PaymentStrategyUPIStrategy ▷ PaymentStrategyPaymentProcessor ◇ PaymentStrategy (1:1)
Final consolidated design showing Strategy pattern structure and relationships.
→ → →

Key Takeaways

✓ Polymorphism replaces conditional logic by allowing interchangeable strategies.

Reading code alone hides how runtime behavior changes; watching the pattern in action clarifies this.

✓ Dependency injection enables flexible strategy assignment and switching at runtime.

Seeing the strategy passed into the processor shows how behavior can be changed without modifying the processor.

✓ The factory centralizes conditional logic, keeping client code clean and focused on polymorphism.

The trace reveals how the factory isolates conditionals, which might be overlooked in static code reading.

Practice

(1/5)
1. In designing a Library Management System, which component is best suited to handle the tracking of book loans and returns to ensure accurate availability status?
easy
A. A separate LoanManager class that manages all loan and return transactions
B. The Library class, which maintains a global list of all books and their statuses
C. The User class, which tracks the books currently borrowed by the user
D. The Book class itself, by maintaining a status flag indicating availability

Solution

  1. Step 1: Understand Single Responsibility Principle

    Tracking loans and returns involves managing transactions and state changes, which is best encapsulated in a dedicated LoanManager to avoid bloating Book or User classes.
  2. Step 2: Why not Book class?

    Embedding availability logic in Book violates SRP and complicates concurrency handling.
  3. Step 3: Why not User class?

    User should track borrowed books but not manage loan state transitions globally.
  4. Step 4: Why not Library class?

    Library manages collections but delegating loan logic to a specialized manager improves modularity and concurrency control.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    LoanManager centralizes loan logic, enabling better consistency and easier concurrency handling.
Hint: Separate transaction logic from data entities for clarity and concurrency [OK]
Common Mistakes:
  • Putting loan logic inside Book class
  • Letting User class handle global loan state
  • Overloading Library class with transaction details
2. Given the following Python code using deep copy, what will be printed after modifying the copy's scores list?
import copy

class Profile:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def __deepcopy__(self, memo):
        new_name = copy.deepcopy(self.name, memo)
        new_scores = copy.deepcopy(self.scores, memo)
        return Profile(new_name, new_scores)

original = Profile('Alice', [10, 20])
copy_obj = copy.deepcopy(original)
print('Original scores:', original.scores)
print('Copy scores:', copy_obj.scores)
copy_obj.scores.append(30)
print('After modifying copy scores:')
print('Original scores:', original.scores)
print('Copy scores:', copy_obj.scores)
easy
A. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20, 30] Copy scores: [10, 20, 30]
B. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20, 30]
C. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20]
D. Original scores: [10, 20] Copy scores: [10, 20, 30] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20, 30]

Solution

  1. Step 1: Trace initial print statements

    Both original.scores and copy_obj.scores start as [10, 20], so first two prints show identical lists.
  2. Step 2: Trace modification and final prints

    copy_obj.scores.append(30) modifies only the copy's scores list because deep copy created a new list. Original remains [10, 20]. Final prints reflect this separation.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Deep copy prevents shared nested list -> original unchanged [OK]
Hint: Deep copy isolates nested mutable objects [OK]
Common Mistakes:
  • Assuming append affects original due to shared reference
3. Which of the following statements about handling concurrency in the Library Management System's LoanManager is INCORRECT?
medium
A. Optimistic locking is unsuitable because it always leads to deadlocks in high contention scenarios.
B. Using pessimistic locking on book records ensures no two users can borrow the same book simultaneously.
C. Implementing atomic check-and-update operations prevents race conditions during loan processing.
D. Using version numbers or timestamps can help detect conflicting updates in optimistic concurrency control.

Solution

  1. Step 1: Understand pessimistic locking

    Pessimistic locking prevents concurrent conflicting access, so Using pessimistic locking on book records ensures no two users can borrow the same book simultaneously. is correct.
  2. Step 2: Why optimistic locking is not always deadlock?

    Optimistic locking is unsuitable because it always leads to deadlocks in high contention scenarios. is incorrect; optimistic locking reduces deadlocks by retrying on conflicts.
  3. Step 3: Atomic operations prevent race conditions

    Implementing atomic check-and-update operations prevents race conditions during loan processing. is correct and essential for concurrency safety.
  4. Step 4: Versioning aids optimistic concurrency

    Using version numbers or timestamps can help detect conflicting updates in optimistic concurrency control. correctly describes conflict detection mechanisms.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    Optimistic locking reduces deadlocks; it does not always cause them.
Hint: Optimistic locking avoids deadlocks by retrying, pessimistic locking blocks access [OK]
Common Mistakes:
  • Believing optimistic locking always causes deadlocks
  • Confusing locking types and their effects
  • Ignoring atomicity in concurrency control
4. If the Snake and Ladder game is extended to support multiple concurrent games running in parallel, which design consideration is most critical to avoid shared state bugs?
hard
A. Ensuring each game instance has its own independent GameController and Player objects
B. Centralizing player positions in a global data structure accessible by all games
C. Sharing the Dice instance across games to reduce resource usage
D. Using a singleton pattern for the Board class to ensure consistency across games

Solution

  1. Step 1: Singleton Board pattern

    Incorrect: Singleton would cause shared state across games, leading to conflicts.
  2. Step 2: Independent GameController and Player per game

    Correct: Isolates state per game, preventing interference.
  3. Step 3: Sharing Dice instance

    Incorrect: Shared Dice can cause race conditions or inconsistent rolls.
  4. Step 4: Global player positions

    Incorrect: Global state breaks isolation and causes bugs.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    Isolating state per game instance is key for concurrency safety.
Hint: Isolate state per game instance to avoid concurrency bugs [OK]
Common Mistakes:
  • Using singleton for shared components without considering concurrency
  • Sharing mutable objects like Dice across threads
  • Centralizing state globally ignoring isolation
5. If a system uses Dependency Injection (DI) extensively but experiences runtime errors due to missing dependencies, what advanced approach can help detect these issues earlier and improve robustness?
hard
A. Manually instantiate all dependencies in client code to ensure correctness.
B. Rely solely on runtime exception handling to catch missing dependencies.
C. Avoid using interfaces and inject concrete classes directly to reduce complexity.
D. Use compile-time dependency injection frameworks or static analysis tools to verify dependency graphs.

Solution

  1. Step 1: Identify the problem

    Runtime errors from missing dependencies indicate lack of early validation.
  2. Step 2: Evaluate solutions

    Use compile-time dependency injection frameworks or static analysis tools to verify dependency graphs. suggests compile-time DI or static analysis, which can catch missing dependencies before runtime. Rely solely on runtime exception handling to catch missing dependencies. defers detection to runtime, which is less robust. Avoid using interfaces and inject concrete classes directly to reduce complexity. breaks DIP and reduces flexibility. Manually instantiate all dependencies in client code to ensure correctness. defeats DI benefits and increases coupling.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Compile-time checks improve reliability by catching DI issues early.
Hint: Use compile-time DI or static analysis to catch missing dependencies early.
Common Mistakes:
  • Relying only on runtime exceptions
  • Injecting concrete classes instead of abstractions
  • Manual instantiation defeating DI benefits
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard