Overview Software design often forces a choice between two fundamental relationships: **is-a** and **has-a**. While inheritance allows a class to derive behavior from a parent, composition involves building complex objects by combining simpler, independent pieces. This guide demonstrates how to refactor rigid inheritance hierarchies into flexible, composed systems. By favoring composition, you reduce the strong coupling that often makes deep inheritance trees brittle and difficult to maintain. We will transform a cluttered employee payment system into a modular architecture where responsibilities like personnel data, payment logic, and commission structures remain distinct and interchangeable. Prerequisites To follow this tutorial, you should have a solid grasp of Python basics, including classes and methods. Familiarity with Object-Oriented Programming (OOP) concepts like classes and subclasses is necessary. Understanding the basics of Abstract Base Classes (ABCs) will help in understanding how we define interfaces. Key Libraries & Tools * **abc**: Python's built-in module for defining Abstract Base Classes, used to enforce method implementation in subclasses. * **dataclasses**: A utility for automatically generating boilerplate code like `__init__` for data-heavy classes. * **typing**: Specifically the `Optional` type, used to handle components that may not exist for every instance. Code Walkthrough The Problem: Inheritance Explosion Initially, developers often reach for inheritance. If you have an `Employee` and need to add a commission, you might create a `SalariedEmployeeWithCommission`. When you add a bonus, you create a `SalariedEmployeeWithCommissionAndBonus`. This leads to a combinatorial explosion of subclasses. The Solution: Modular Composition First, we define our interfaces using Abstract Base Classes. Instead of one giant hierarchy, we create separate modules for different responsibilities. ```python from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Optional class Contract(ABC): @abstractmethod def get_payment(self) -> float: pass class Commission(ABC): @abstractmethod def get_payment(self) -> float: pass ``` Next, we implement concrete versions of these components. This isolates the logic for hourly pay from the logic for sales commissions. ```python @dataclass class HourlyContract(Contract): pay_rate: float hours_worked: float employer_cost: float = 1000 def get_payment(self) -> float: return self.pay_rate * self.hours_worked + self.employer_cost @dataclass class ContractCommission(Commission): commission_amount: float contracts_landed: int def get_payment(self) -> float: return self.commission_amount * self.contracts_landed ``` Finally, the `Employee` class becomes a container. It doesn't care *how* pay is calculated; it simply delegates that task to its composed components. ```python @dataclass class Employee: name: string id: int contract: Contract commission: Optional[Commission] = None def compute_pay(self) -> float: payout = self.contract.get_payment() if self.commission: payout += self.commission.get_payment() return payout ``` Syntax Notes We utilize the `@dataclass` decorator to keep the code clean and readable, removing the need for manual `__init__` methods. The `Optional[Commission]` syntax indicates that while every employee must have a contract, the commission component is a flexible add-on. This pattern adheres to the Dependency Inversion principle, as the `Employee` depends on the `Contract` abstraction rather than a concrete implementation. Practical Examples This pattern is highly effective in FinTech applications where payment rules change frequently. By swapping a `SalaryContract` for an `HourlyContract` at runtime, you can change an object's behavior without modifying its class. It is also prevalent in game development (ECS patterns) where entities gain abilities by adding components rather than inheriting from a "Super Warrior" class. Tips & Gotchas Avoid deep inheritance chains; most design patterns from the Gang of Four rarely exceed one or two levels. Inheritance creates the strongest possible coupling in OOP, meaning a change in the parent can silently break every child. If you find yourself using `super()` to constantly patch behavior, you likely need composition. Use Abstract Base Classes to define clear boundaries between your system's modules.
Dependency Injection
Softwaredesign
- Jun 11, 2021