composition_vs_inheritance.py

Download
python 479 lines 11.9 KB
  1"""
  2Composition vs Inheritance
  3
  4This demonstrates why "Favor Composition over Inheritance" is a key OOP principle.
  5
  6Problems with deep inheritance hierarchies:
  71. Tight coupling
  82. Fragile base class problem
  93. Diamond problem (multiple inheritance)
 104. Difficult to reason about
 115. Hard to extend without breaking existing code
 12
 13Composition provides:
 141. Loose coupling
 152. Flexibility
 163. Better testability
 174. Easier to understand
 18"""
 19
 20from abc import ABC, abstractmethod
 21from typing import List, Optional
 22
 23
 24# =============================================================================
 25# PROBLEM: Inheritance Hierarchy Gets Out of Control
 26# =============================================================================
 27
 28print("=" * 70)
 29print("THE INHERITANCE PROBLEM")
 30print("=" * 70)
 31
 32
 33# ❌ BAD: Deep inheritance hierarchy
 34class Animal:
 35    """Base class"""
 36
 37    def __init__(self, name: str):
 38        self.name = name
 39
 40    def eat(self):
 41        print(f"{self.name} is eating...")
 42
 43
 44class FlyingAnimal(Animal):
 45    """Animals that can fly"""
 46
 47    def fly(self):
 48        print(f"{self.name} is flying...")
 49
 50
 51class SwimmingAnimal(Animal):
 52    """Animals that can swim"""
 53
 54    def swim(self):
 55        print(f"{self.name} is swimming...")
 56
 57
 58# Problem 1: What about a duck that can both fly AND swim?
 59# We'd need multiple inheritance...
 60class Duck(FlyingAnimal, SwimmingAnimal):
 61    """Duck can fly and swim - uses multiple inheritance"""
 62
 63    pass
 64
 65
 66# Problem 2: What about a penguin? It swims but doesn't fly!
 67class Penguin(SwimmingAnimal):
 68    """Penguin swims but doesn't fly"""
 69
 70    pass
 71
 72
 73# But what if we made Penguin inherit from FlyingAnimal by mistake?
 74class PenguinBad(FlyingAnimal):
 75    """Inherits fly() but penguins can't fly!"""
 76
 77    def fly(self):
 78        # Have to override and raise error or do nothing
 79        raise NotImplementedError("Penguins can't fly!")
 80
 81
 82# Problem 3: What about a robotic dog? It's not an Animal!
 83# But we want similar behavior...
 84# Can't inherit from Animal (it's not biological)
 85# Have to duplicate code or create weird hierarchies
 86
 87
 88# =============================================================================
 89# SOLUTION: Composition over Inheritance
 90# =============================================================================
 91
 92print("\n" + "=" * 70)
 93print("THE COMPOSITION SOLUTION")
 94print("=" * 70)
 95
 96
 97# ✅ GOOD: Define behaviors as separate components
 98class FlyingBehavior(ABC):
 99    """Abstract flying behavior"""
100
101    @abstractmethod
102    def fly(self, name: str):
103        pass
104
105
106class CanFly(FlyingBehavior):
107    """Concrete flying implementation"""
108
109    def fly(self, name: str):
110        print(f"{name} is flying high in the sky!")
111
112
113class CannotFly(FlyingBehavior):
114    """Concrete non-flying implementation"""
115
116    def fly(self, name: str):
117        print(f"{name} can't fly.")
118
119
120class SwimmingBehavior(ABC):
121    """Abstract swimming behavior"""
122
123    @abstractmethod
124    def swim(self, name: str):
125        pass
126
127
128class CanSwim(SwimmingBehavior):
129    """Concrete swimming implementation"""
130
131    def swim(self, name: str):
132        print(f"{name} is swimming gracefully!")
133
134
135class CannotSwim(SwimmingBehavior):
136    """Concrete non-swimming implementation"""
137
138    def swim(self, name: str):
139        print(f"{name} can't swim.")
140
141
142class EatingBehavior(ABC):
143    """Abstract eating behavior"""
144
145    @abstractmethod
146    def eat(self, name: str):
147        pass
148
149
150class Herbivore(EatingBehavior):
151    def eat(self, name: str):
152        print(f"{name} is eating plants...")
153
154
155class Carnivore(EatingBehavior):
156    def eat(self, name: str):
157        print(f"{name} is eating meat...")
158
159
160class Omnivore(EatingBehavior):
161    def eat(self, name: str):
162        print(f"{name} is eating everything...")
163
164
165# Now compose behaviors into entities
166class ComposedAnimal:
167    """Animal with composable behaviors"""
168
169    def __init__(
170        self,
171        name: str,
172        flying_behavior: FlyingBehavior,
173        swimming_behavior: SwimmingBehavior,
174        eating_behavior: EatingBehavior,
175    ):
176        self.name = name
177        self._flying_behavior = flying_behavior
178        self._swimming_behavior = swimming_behavior
179        self._eating_behavior = eating_behavior
180
181    def fly(self):
182        """Delegate to flying behavior"""
183        self._flying_behavior.fly(self.name)
184
185    def swim(self):
186        """Delegate to swimming behavior"""
187        self._swimming_behavior.swim(self.name)
188
189    def eat(self):
190        """Delegate to eating behavior"""
191        self._eating_behavior.eat(self.name)
192
193    def set_flying_behavior(self, behavior: FlyingBehavior):
194        """Can change behavior at runtime!"""
195        self._flying_behavior = behavior
196
197
198# =============================================================================
199# REAL-WORLD EXAMPLE: Employee Management System
200# =============================================================================
201
202print("\n" + "=" * 70)
203print("REAL-WORLD EXAMPLE: Employee System")
204print("=" * 70)
205
206
207# ❌ BAD: Inheritance-based approach
208class EmployeeBad:
209    def __init__(self, name: str, salary: float):
210        self.name = name
211        self.salary = salary
212
213    def calculate_bonus(self) -> float:
214        return self.salary * 0.1
215
216
217class ManagerBad(EmployeeBad):
218    def calculate_bonus(self) -> float:
219        return self.salary * 0.2
220
221
222class DeveloperBad(EmployeeBad):
223    def code(self):
224        print(f"{self.name} is coding...")
225
226
227class DesignerBad(EmployeeBad):
228    def design(self):
229        print(f"{self.name} is designing...")
230
231
232# Problem: What about a developer who becomes a manager?
233# Or someone who codes AND designs?
234# Inheritance forces rigid categories!
235
236
237# ✅ GOOD: Composition-based approach
238class BonusCalculator(ABC):
239    @abstractmethod
240    def calculate(self, salary: float) -> float:
241        pass
242
243
244class StandardBonus(BonusCalculator):
245    def calculate(self, salary: float) -> float:
246        return salary * 0.1
247
248
249class ManagerBonus(BonusCalculator):
250    def calculate(self, salary: float) -> float:
251        return salary * 0.2
252
253
254class SeniorBonus(BonusCalculator):
255    def calculate(self, salary: float) -> float:
256        return salary * 0.25
257
258
259class Skill(ABC):
260    """Represents a skill an employee can have"""
261
262    @abstractmethod
263    def perform(self, name: str):
264        pass
265
266
267class CodingSkill(Skill):
268    def perform(self, name: str):
269        print(f"{name} is coding...")
270
271
272class DesignSkill(Skill):
273    def perform(self, name: str):
274        print(f"{name} is designing...")
275
276
277class ManagementSkill(Skill):
278    def perform(self, name: str):
279        print(f"{name} is managing the team...")
280
281
282class Employee:
283    """Flexible employee with composable skills and bonus calculation"""
284
285    def __init__(
286        self,
287        name: str,
288        salary: float,
289        bonus_calculator: BonusCalculator,
290        skills: Optional[List[Skill]] = None,
291    ):
292        self.name = name
293        self.salary = salary
294        self._bonus_calculator = bonus_calculator
295        self._skills = skills or []
296
297    def calculate_bonus(self) -> float:
298        return self._bonus_calculator.calculate(self.salary)
299
300    def add_skill(self, skill: Skill):
301        """Can add skills dynamically!"""
302        self._skills.append(skill)
303
304    def perform_skills(self):
305        """Execute all skills"""
306        for skill in self._skills:
307            skill.perform(self.name)
308
309    def promote(self, new_bonus_calculator: BonusCalculator):
310        """Change bonus calculation (e.g., promotion)"""
311        self._bonus_calculator = new_bonus_calculator
312
313
314# =============================================================================
315# DEMONSTRATION
316# =============================================================================
317
318def demonstrate_inheritance_problems():
319    print("\n[INHERITANCE APPROACH]")
320    print("-" * 50)
321
322    duck = Duck("Donald")
323    duck.eat()
324    duck.fly()
325    duck.swim()
326
327    penguin = Penguin("Pingu")
328    penguin.eat()
329    penguin.swim()
330    # penguin.fly()  # Doesn't have fly method
331
332    penguin_bad = PenguinBad("BadPingu")
333    try:
334        penguin_bad.fly()  # Will raise error
335    except NotImplementedError as e:
336        print(f"Error: {e}")
337
338    print("\nProblems with inheritance:")
339    print("  ✗ Rigid hierarchies")
340    print("  ✗ Hard to mix behaviors (flying + swimming)")
341    print("  ✗ Breaking Liskov Substitution Principle")
342    print("  ✗ Can't change behavior at runtime")
343
344
345def demonstrate_composition_solution():
346    print("\n[COMPOSITION APPROACH]")
347    print("-" * 50)
348
349    # Duck: can fly, can swim, eats everything
350    duck = ComposedAnimal(
351        "Donald",
352        flying_behavior=CanFly(),
353        swimming_behavior=CanSwim(),
354        eating_behavior=Omnivore(),
355    )
356    duck.fly()
357    duck.swim()
358    duck.eat()
359
360    # Penguin: cannot fly, can swim, eats fish
361    penguin = ComposedAnimal(
362        "Pingu",
363        flying_behavior=CannotFly(),
364        swimming_behavior=CanSwim(),
365        eating_behavior=Carnivore(),
366    )
367    penguin.fly()
368    penguin.swim()
369    penguin.eat()
370
371    # Robot bird: can fly (with motors), cannot swim, doesn't eat
372    robot = ComposedAnimal(
373        "RoboBird",
374        flying_behavior=CanFly(),
375        swimming_behavior=CannotSwim(),
376        eating_behavior=Herbivore(),  # Charges battery instead
377    )
378    robot.fly()
379
380    # Change behavior at runtime!
381    print("\n[RUNTIME BEHAVIOR CHANGE]")
382    print("Penguin learns to fly (with jetpack):")
383    penguin.set_flying_behavior(CanFly())
384    penguin.fly()
385
386    print("\nBenefits of composition:")
387    print("  ✓ Flexible behavior mixing")
388    print("  ✓ Runtime behavior changes")
389    print("  ✓ Easy to test (mock behaviors)")
390    print("  ✓ Follows Open/Closed Principle")
391    print("  ✓ No inheritance coupling")
392
393
394def demonstrate_employee_system():
395    print("\n[EMPLOYEE SYSTEM EXAMPLE]")
396    print("-" * 50)
397
398    # Junior developer
399    alice = Employee(
400        "Alice",
401        salary=60000,
402        bonus_calculator=StandardBonus(),
403        skills=[CodingSkill()],
404    )
405    alice.perform_skills()
406    print(f"Bonus: ${alice.calculate_bonus()}")
407
408    # Senior developer who also designs
409    bob = Employee(
410        "Bob",
411        salary=90000,
412        bonus_calculator=SeniorBonus(),
413        skills=[CodingSkill(), DesignSkill()],
414    )
415    print(f"\n{bob.name}'s skills:")
416    bob.perform_skills()
417    print(f"Bonus: ${bob.calculate_bonus()}")
418
419    # Promote Alice to manager
420    print(f"\n{alice.name} gets promoted to manager!")
421    alice.promote(ManagerBonus())
422    alice.add_skill(ManagementSkill())
423    alice.perform_skills()
424    print(f"New bonus: ${alice.calculate_bonus()}")
425
426    print("\nComposition allows:")
427    print("  ✓ Employees with multiple skills")
428    print("  ✓ Dynamic skill addition")
429    print("  ✓ Easy promotions (change bonus calculator)")
430    print("  ✓ No rigid job categories")
431
432
433def print_comparison():
434    print("\n" + "=" * 70)
435    print("COMPOSITION VS INHERITANCE")
436    print("=" * 70)
437
438    print("""
439WHEN TO USE INHERITANCE:
440  ✓ True "is-a" relationship (Cat IS-A Animal)
441  ✓ Shared implementation across subtypes
442  ✓ Shallow hierarchies (1-2 levels)
443  ✓ Liskov Substitution Principle holds
444
445WHEN TO USE COMPOSITION:
446  ✓ "has-a" or "can-do" relationships
447  ✓ Need to mix multiple behaviors
448  ✓ Want to change behavior at runtime
449  ✓ Need flexibility and loose coupling
450  ✓ Want to avoid fragile base class problem
451
452KEY PRINCIPLE:
453  "Favor composition over inheritance"
454
455  This doesn't mean NEVER use inheritance.
456  It means: think carefully before inheriting,
457  and consider if composition is better.
458
459COMPOSITION ADVANTAGES:
460  • More flexible
461  • Better testability
462  • Easier to reason about
463  • No deep hierarchies
464  • Runtime behavior changes
465  • Follows SOLID principles better
466
467INHERITANCE ADVANTAGES:
468  • More intuitive for true "is-a" relationships
469  • Less boilerplate for simple cases
470  • Built-in language support
471""")
472
473
474if __name__ == "__main__":
475    demonstrate_inheritance_problems()
476    demonstrate_composition_solution()
477    demonstrate_employee_system()
478    print_comparison()