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 Shape Class Using Inheritance
We start by defining a base class 'Shape' with a method 'draw'. This sets up the inheritance approach where specific shapes will extend this base class.
💡 Initializing the base class is crucial because it establishes the common interface for all shapes in the inheritance model.
Line:class Shape {
public void draw() {}
}
💡 This step shows how inheritance begins with a common superclass defining shared behavior.
fill_row
Create Derived Circle Class Extending Shape
We define a 'Circle' class that inherits from 'Shape' and overrides the 'draw' method to provide specific behavior.
💡 This step shows how inheritance allows specialization by overriding base class methods.
Line:class Circle extends Shape {
@Override
public void draw() {
// draw circle
}
}
💡 Inheritance enables code reuse but tightly couples Circle to Shape's interface.
setup
Add Renderer Interface for Composition
We define a 'Renderer' interface with a 'render' method. This interface will be used to compose rendering behavior into shapes.
💡 Introducing an interface separates rendering behavior from shape hierarchy, enabling flexible composition.
Line:interface Renderer {
void render();
}
💡 This step sets up the composition approach by defining a role that can be delegated to.
fill_row
Implement Renderer with VectorRenderer
We create 'VectorRenderer' class implementing 'Renderer' interface, providing vector-based rendering logic.
💡 This concrete implementation shows how different rendering strategies can be composed.
We define 'Circle' class extending 'Shape' and using the composed 'Renderer' to draw itself.
💡 This step demonstrates how composition allows shape classes to reuse rendering behavior flexibly.
Line:class Circle extends Shape {
public Circle(Renderer renderer) {
super(renderer);
}
@Override
public void draw() {
renderer.render();
}
}
💡 Composition enables changing rendering behavior at runtime by passing different Renderer instances.
insert
Instantiate VectorRenderer and Compose with Circle
We create an instance of 'VectorRenderer' and pass it to a new 'Circle' instance, composing rendering behavior.
💡 This step shows how composition allows runtime binding of behavior implementations.
Line:Renderer renderer = new VectorRenderer();
Shape circle = new Circle(renderer);
💡 Composition provides flexibility to change rendering without modifying shape classes.
traverse
Call draw() on Circle Using Inheritance
We invoke the 'draw' method on the Circle instance that inherits from Shape, executing the overridden method.
💡 This step shows how polymorphism works in inheritance to call the correct method.
Line:circle.draw();
💡 Inheritance binds behavior at compile time, limiting flexibility to change rendering dynamically.
traverse
Call draw() on Circle Using Composition
We invoke the 'draw' method on the Circle instance composed with a Renderer, which delegates rendering to the Renderer instance.
💡 This step highlights how composition delegates behavior dynamically to the composed object.
Line:circle.draw();
💡 Composition allows changing rendering behavior by passing different Renderer implementations at runtime.
prune
Summary: Why Favor Composition Over Inheritance
We conclude by highlighting that composition offers greater flexibility, easier maintenance, and better separation of concerns compared to inheritance.
💡 This final step synthesizes the visualized differences into a clear design guideline.
💡 Favoring composition reduces tight coupling and allows behavior to be changed at runtime without modifying class hierarchies.
Composition vs Inheritance - Favour Composition, Why & When - Watch the Algorithm Execute, Step by Step
Watching this visualization helps you understand the structural and behavioral differences between inheritance and composition without needing to read dense theory. You see how design decisions affect class relationships and extensibility.
Circle ▷ Shape (1:1)Shape ◆ Renderer (1:1)VectorRenderer → Renderer (1:1)
Key Takeaways
✓ Composition decouples behavior from class hierarchies, enabling flexible and dynamic behavior changes.
This insight is hard to see from code alone because the delegation and runtime binding are implicit; visualization makes it explicit.
✓ Inheritance tightly couples subclasses to base class interfaces, limiting extensibility and increasing fragility.
Seeing the fixed inheritance arrows and overridden methods side-by-side clarifies why inheritance can be restrictive.
✓ Using interfaces and composition promotes separation of concerns and adheres to SOLID principles.
Visualizing interfaces and composition relationships helps understand how design principles are applied in practice.
Practice
(1/5)
1. You need to create different types of vehicles (cars, bikes) that share a common interface, and sometimes you want to create entire families of related vehicles (e.g., electric car + electric bike) ensuring consistency. Which design pattern best fits this requirement?
easy
A. Builder Pattern, because it constructs complex objects step-by-step.
B. Abstract Factory Pattern, because it creates families of related objects ensuring consistency.
C. Factory Pattern, because it creates simple objects based on a type parameter.
D. Singleton Pattern, because it ensures only one instance of each vehicle type.
Solution
Step 1: Understand the requirement for families of related objects
The problem states the need to create related vehicles (e.g., electric car and electric bike) that belong to a family and must be consistent.
Step 2: Match pattern to requirement
Abstract Factory is designed to create families of related objects, ensuring that the created objects are compatible and consistent.
Final Answer:
Option B -> Option B
Quick Check:
Abstract Factory creates related object families, Factory creates single objects [OK]
Hint: Families of related objects -> Abstract Factory [OK]
Common Mistakes:
Confusing Factory with Abstract Factory
Using Builder for simple object creation
2. When a class inherits from multiple classes that have a method with the same name, describe the step-by-step process the Method Resolution Order (MRO) uses to determine which method is called.
easy
A. MRO uses a linearization algorithm that merges the order of parents and their ancestors to find the method.
B. MRO searches the first parent class fully before moving to the next parent class.
C. MRO always calls the method from the last parent class listed in the inheritance.
D. MRO randomly picks the method from any parent class that defines it.
Solution
Step 1: Understand naive search
MRO does not simply search the first parent class fully before moving to the next; it uses a more sophisticated approach.
Step 2: Recognize MRO linearization
MRO uses a specific linearization (like C3 linearization) that merges parent classes and their ancestors in a consistent order.
Step 3: Eliminate incorrect options
MRO always calls the method from the last parent class listed in the inheritance is incorrect because the last parent is not always chosen; order and ancestors matter. MRO randomly picks the method from any parent class that defines it is incorrect because MRO is deterministic, not random.
Final Answer:
Option A -> Option A
Quick Check:
MRO merges inheritance hierarchies to find the correct method in a predictable order.
Hint: MRO = deterministic linearization of inheritance graph
Common Mistakes:
Assuming simple left-to-right search suffices
Believing last parent always overrides
Thinking method choice is random
3. What is the time complexity of processing a payment using the optimal strategy pattern implementation with dependency injection, assuming the payment method is already selected?
medium
A. O(n) where n is the number of payment methods
B. O(log n) due to searching the strategy in a factory method
C. O(1) constant time since the strategy is injected and pay() is a direct call
D. O(n^2) because of nested conditional checks inside the strategy classes
Solution
Step 1: Identify the operation performed during payment
Once the strategy is injected, calling pay() is a direct method call without iteration or recursion.
Step 2: Analyze complexity of the pay() method
The pay() method executes a simple print statement, which is O(1).
Final Answer:
Option C -> Option C
Quick Check:
Direct method call with no loops or recursion is constant time [OK]
Hint: Injected strategy enables direct O(1) method call [OK]
Common Mistakes:
Assuming factory lookup or conditionals happen at payment time
4. If you want to allow decorators to be added and removed dynamically at runtime (e.g., undo last added decorator), which modification to the Decorator Pattern implementation is most appropriate?
hard
A. Modify the base class to include flags for each decorator feature
B. Use inheritance to create new subclasses for every add/remove combination
C. Store decorators in a stack data structure and update the wrapped object reference on add/remove
D. Cache all possible decorator combinations in a lookup table
Solution
Step 1: Understand dynamic add/remove requirement
Need to add or remove decorators at runtime without subclass explosion or code changes.
Step 2: Identify suitable data structure and pattern modification
Using a stack to track decorators allows pushing and popping decorators, updating the wrapped object reference accordingly.
Step 3: Evaluate other options
Inheritance and flags lead to rigidity; caching all combinations is infeasible for many decorators.
Hint: Stack supports dynamic add/remove of decorators [OK]
Common Mistakes:
Trying to use inheritance for dynamic behavior
Modifying base class breaks open-closed principle
5. In a legacy system, a class handles both business logic and logging. You want to refactor it following SRP, but the logging code is tightly intertwined with business logic. What is the best approach to refactor this while respecting SRP?
hard
A. Ignore SRP in this case because legacy code should not be refactored.
B. Leave logging inside the class because extracting it would break existing functionality and increase risk.
C. Merge business logic and logging into a utility class to centralize all cross-cutting concerns.
D. Extract logging into a separate class and replace logging calls with calls to this new class, even if it requires modifying many places.
Solution
Step 1: Identify responsibilities
Business logic and logging are separate reasons to change.
Step 2: Refactor strategy
Extract logging into its own class to isolate changes related to logging.
Step 3: Address tight coupling
Though intertwined, refactoring calls to the new logging class improves maintainability and respects SRP.
Step 4: Evaluate other options
Options B and D avoid refactoring, risking future issues; C increases coupling by merging unrelated concerns.
Final Answer:
Option D -> Option D
Quick Check:
Extract and isolate responsibilities even if it requires effort -> SRP compliance.
Hint: Separate concerns even if intertwined; refactor stepwise.