Welcome to the architect's workshop. Today, we dismantle the rigid structures of the past. You have likely been taught that Inheritance is the cornerstone of Object-Oriented Programming. As a Senior Architect, I tell you this: Inheritance is a trap. It creates fragile hierarchies that crumble under change. Today, we master Composition—the art of building flexible systems by assembling small, independent parts.
The Architectural Shift: Is-A vs. Has-A
The Inheritance Trap (Is-A)
When you use inheritance, you are saying "A Dog is a Mammal." This works until you need a "Flying Dog." Suddenly, your hierarchy breaks. You end up with deep, brittle trees where a change in the parent ripples down to destroy the child.
The Composition Solution (Has-A)
Composition says "A Car has an Engine." It doesn't care what kind of engine it is. You can swap a V8 for an Electric Motor at runtime. This is the Composition over Inheritance principle in action.
Visualizing the Difference
Look closely at the diagrams below. On the left, notice the rigid vertical dependency. On the right, observe the horizontal, modular connections.
Implementation: Building a Dynamic System
Let's write code. We will build a Robot class. Instead of inheriting from WalkingRobot or FlyingRobot, we will compose it from behaviors. This allows us to mix and match capabilities dynamically.
# Define independent behaviors (The Parts) class Flyable: def fly(self): return "🚁 Soaring through the sky!" class Walkable: def walk(self): return "🦶 Walking on two legs." class WeaponSystem: def fire(self): return "🔥 Laser blast!" # The Main Class (The Assembly) class Robot: def __init__(self, name, behaviors): self.name = name # Composition: We inject behaviors at runtime self.behaviors = behaviors def perform_action(self): results = [] for behavior in self.behaviors: # Delegate work to the component if hasattr(behavior, 'fly'): results.append(behavior.fly()) if hasattr(behavior, 'walk'): results.append(behavior.walk()) if hasattr(behavior, 'fire'): results.append(behavior.fire()) return results # --- Usage --- # Scenario A: A peaceful delivery bot delivery_bot = Robot("Droid-X", [Walkable()]) print(f"{delivery_bot.name}: {delivery_bot.perform_action()}") # Scenario B: A combat bot combat_bot = Robot("War-Mech", [Flyable(), Walkable(), WeaponSystem()]) print(f"{combat_bot.name}: {combat_bot.perform_action()}") Why This Matters for Scalability
Consider the complexity of maintenance. In a deep inheritance tree, the complexity grows exponentially. With composition, the complexity is linear. You simply add another component to the list. This concept is similar to how decorators in Python wrap functionality dynamically.
Key Takeaways
- Prefer "Has-A" over "Is-A": Use composition to build systems from parts rather than extending base classes.
- Runtime Flexibility: Composition allows you to change behavior while the program is running, not just at compile time.
- Testability: Small, composed components are easier to unit test in isolation than a massive inherited class.
Understanding the Has-A Relationship in Object Design
In the architecture of robust software, the most dangerous question a developer can ask is, "What is this class?" The better question is, "What does this class contain?"
This shift in perspective moves you from the rigid trap of Inheritance (Is-A) to the flexible power of Composition (Has-A). When you design a system where objects are built by assembling smaller, specialized components, you create software that is resilient, testable, and infinitely adaptable.
Think of a House. It is not a "super-room." It has rooms. It has a foundation. It has a roof. If you inherit from a Room to make a House, you end up with a structure that is too small to live in. Instead, you compose the House from its parts.
Figure 1: The House aggregates Rooms. The relationship is strong (filled diamond), implying ownership.
The Mechanics of Composition
Composition allows you to swap out behaviors at runtime. If your House needs a new kitchen, you don't rewrite the House class. You simply replace the Kitchen object inside it. This decoupling is the secret sauce of scalable systems.
For a deeper dive into why this pattern often beats inheritance, check out our guide on composition over inheritance practical.
Dynamic Assembly
In a Has-A relationship, the parent object holds references to child objects.
● Encapsulation: The House manages the lifecycle of the Rooms.
● Flexibility: You can add a Garage or remove the Kitchen without breaking the House.
house.removeRoom(oldGarage);
Implementation in Code
Let's look at how this translates to Python. Notice how the Car class doesn't inherit from Engine. It simply owns an engine. This is the essence of the composition vs inheritance making right philosophy.
# The Component (Engine) class Engine: def __init__(self, horsepower): self.horsepower = horsepower def start(self): return f"Vroom! {self.horsepower} HP engine roaring." # The Composite (Car) class Car: def __init__(self, model, horsepower): # HAS-A relationship: Car contains an Engine self.model = model self.engine = Engine(horsepower) def drive(self): # Delegation: Car asks Engine to do the work status = self.engine.start() return f"{self.model} is moving. {status}" # Usage my_sports_car = Car("Porsche 911", 450) print(my_sports_car.drive()) # Output: Porsche 911 is moving. Vroom! 450 HP engine roaring. Key Takeaways
- Prefer "Has-A" over "Is-A": Use composition to build systems from parts rather than extending base classes.
- Runtime Flexibility: Composition allows you to change behavior while the program is running, not just at compile time.
- Testability: Small, composed components are easier to unit test in isolation than a massive inherited class.
Composition vs Inheritance: Choosing the Right Design Strategy
As a Senior Architect, I often see junior developers reach for Inheritance like a hammer, treating every problem as a nail. While inheritance models an "Is-A" relationship, it creates rigid, fragile structures. Composition, on the other hand, models a "Has-A" relationship, offering the flexibility to assemble systems from interchangeable parts.
The Architect's Comparison Matrix
Inheritance (The "Is-A" Trap)
- 🔗 Coupling: High (Tight)
- 🛠 Flexibility: Low (Static)
- 🐛 Risk: Fragile Base Class Problem
- 📈 Complexity: Grows exponentially with depth
Composition (The "Has-A" Win)
- 🔗 Coupling: Low (Decoupled)
- 🛠 Flexibility: High (Dynamic)
- 🐛 Risk: Minimal (Interface-based)
- 📈 Complexity: Linear and manageable
Structural Visualization
The "Fragile Base Class" Problem
When you inherit from a class, you are trusting that the parent will never change its internal logic in a way that breaks your child. This is a dangerous bet.
❌ Inheritance: Tight Coupling
class Set: def __init__(self): self._items = [] def add(self, item): self._items.append(item) def size(self): return len(self._items) # The Trap: Inheriting behavior class CountingSet(Set): def __init__(self): super().__init__() self._count = 0 def add(self, item): # If parent 'add' calls 'add' internally (e.g. for validation), # our counter increments TWICE! super().add(item) self._count += 1
✅ Composition: Controlled Interface
class CountingSet: def __init__(self): # We own the implementation details self._items = [] self._count = 0 def add(self, item): # Explicit control over logic if item not in self._items: self._items.append(item) self._count += 1 def size(self): return self._count
Dynamic Behavior Injection
With composition, you can swap behaviors on the fly. Imagine a NotificationService that can switch between Email, SMS, or Push notifications without changing a single line of the main application code. This is the power of Dependency Injection.
(Visual: The User object is composed of a Behavior object. We can swap "Email" for "SMS" instantly.)
Key Takeaways
- Prefer "Has-A" over "Is-A": Use composition to build systems from parts rather than extending base classes. This reduces coupling significantly.
- Runtime Flexibility: Composition allows you to change behavior while the program is running, not just at compile time.
- Testability: Small, composed components are easier to unit test in isolation than a massive inherited class.
- Further Reading: For a deeper dive into this specific pattern, check out our guide on composition over inheritance practical.
Implementing Composition in Python: A Step-by-Step Guide
Welcome to the workshop floor. Today, we aren't just writing code; we are architecting systems. In the world of professional software engineering, Composition is the art of building complex systems by assembling smaller, specialized components. It is the antidote to the rigid, fragile nature of deep inheritance hierarchies.
Think of a car. It doesn't inherit from an Engine. It has an Engine. It has Brakes. It has a Steering Wheel. By treating these as distinct objects passed into the Car, we achieve a level of flexibility that inheritance simply cannot match. If you want to swap a V8 engine for an Electric motor, you don't rewrite the Car class; you just swap the component.
The Blueprint: Dependency Injection
The mechanism that makes composition work is Dependency Injection. Instead of a class creating its own dependencies (tight coupling), we inject them from the outside. This makes your code modular, testable, and robust.
# 1. Define the Components (The "Parts") class Engine: def __init__(self, type_): self.type = type_ def start(self): return f"{self.type} engine roaring to life." class BrakeSystem: def __init__(self, type_): self.type = type_ def apply(self): return f"{self.type} brakes engaging." # 2. Define the Composite (The "Whole") class Car: def __init__(self, engine, brakes): # Dependency Injection: We receive the parts, we don't create them self.engine = engine self.brakes = brakes def drive(self): # Delegate behavior to the components print(self.engine.start()) return "Car is moving." def stop(self): print(self.brakes.apply()) return "Car has stopped." # 3. Assembly (The "Factory" Logic) # We can mix and match parts dynamically v8_engine = Engine("V8 Petrol") disc_brakes = BrakeSystem("Ceramic Disc") my_car = Car(v8_engine, disc_brakes) print(my_car.drive()) print(my_car.stop()) 💡 Architect's Insight
Notice how the Car class knows nothing about the specific implementation of the engine? It just knows it has an object with a start() method. This is the Open/Closed Principle in action. If you want to learn more about advanced Python patterns that complement this, check out our guide on how to use decorators in python to add behavior without changing class structure.
Why This Matters for Scalability
When you rely on inheritance, you often end up with a "God Class" that tries to do everything. Composition allows you to break functionality into Single Responsibility units. This is critical when you are building concurrent applications or handling complex state management.
You can mock the Engine and test the Car logic in isolation without needing a real engine.
Swap a GasEngine for an ElectricMotor at runtime without recompiling the entire application.
The BrakeSystem can be used in a Truck, a Bike, or a Train with zero code duplication.
Mastering this pattern is essential for modern architecture. If you are struggling with the theoretical differences, I highly recommend reading composition over inheritance practical to see real-world scenarios where inheritance fails and composition saves the day.
Key Takeaways
- Prefer "Has-A" over "Is-A": Use composition when one object is a part of another, rather than a subtype.
- Inject Dependencies: Pass objects into constructors rather than instantiating them internally.
- Delegate Responsibility: Let the component objects handle their own specific logic.
OOP Composition Example: Constructing a Vehicle from Parts
As a Senior Architect, I often see junior developers try to model the world using deep, tangled inheritance trees. They ask, "Is a Car a Vehicle? Yes. Is a SportsCar a Car? Yes." But then they hit a wall: "Is a Car an Engine?" No. A Car has an Engine. This is the fundamental shift from "Is-A" to "Has-A".
In this masterclass, we aren't just defining classes; we are assembling a system. We will build a Vehicle by composing distinct, reusable components: an Engine, a set of Wheels, and a Chassis. This approach creates systems that are easier to test, maintain, and scale.
The Composition Blueprint
Notice the filled diamond shapes (◆). This symbolizes strong ownership. If the Vehicle is destroyed, its parts are often destroyed with it.
Visualizing the Assembly
Watch how a complex object is constructed from simple, independent units. In a real application, this might represent dependency injection or constructor initialization.
Vehicle Object
(In a live environment, this animation would trigger on load, assembling the parts into the container.)
The Implementation (Python)
# 1. Define the Components (The Parts) class Engine: def __init__(self, horsepower): self.horsepower = horsepower def start(self): return f"Vroom! {self.horsepower} HP engine ignited." class Wheel: def __init__(self, size): self.size = size def rotate(self): return f"Wheel {self.size} inch rotating." # 2. Define the Composite Object (The Whole) class Vehicle: def __init__(self, engine_hp, wheel_size): # Composition: The Vehicle 'has-a' Engine and 'has-a' Wheel self.engine = Engine(engine_hp) self.wheels = [Wheel(wheel_size) for _ in range(4)] def drive(self): # Delegate responsibility to components print(self.engine.start()) for wheel in self.wheels: print(wheel.rotate()) # 3. Usage my_car = Vehicle(300, 18) my_car.drive()
Why This Matters
In the code above, the Vehicle class does not know how to start an engine or how to rotate a wheel. It simply delegates these tasks. This is the essence of Encapsulation and Single Responsibility Principle.
If you need to upgrade the engine, you simply swap the Engine object passed into the constructor. You don't need to rewrite the Vehicle class. This flexibility is why we prefer composition over inheritance in modern software architecture.
Key Takeaways
- Has-A vs Is-A: Use composition when an object is a collection of parts, not a subtype of another.
- Delegate Logic: Let the component objects (Engine, Wheel) handle their own specific behaviors.
- Loose Coupling: By injecting dependencies (like passing an Engine into the Vehicle), you make your code modular and testable.
Composition Over Inheritance: Why Flexibility Wins
In the early days of Object-Oriented Programming (OOP), inheritance was the golden hammer. If you needed a new type of object, you simply extended an existing one. But as systems grew, we hit the "Brittle Base Class" problem. A change in the parent class could silently break a dozen subclasses.
Today, we embrace a more robust philosophy: Composition over Inheritance. Instead of defining what an object is (Is-A relationship), we define what an object has (Has-A relationship). This approach decouples your system, making it modular, testable, and resilient to change.
The Architectural Shift
❌ Inheritance (Rigid)
Deep hierarchies create tight coupling. Changing the parent affects all children.
✅ Composition (Flexible)
Objects are built by assembling smaller, independent components.
The "Black Box" Problem
Inheritance exposes the internal implementation of the parent class. If the parent changes its internal logic, the child breaks. This is known as the fragile base class problem.
Composition solves this by treating components as "black boxes." You only care about the interface (the public methods), not the internal state. This is crucial when building complex systems like concurrent applications, where state management is critical.
# ❌ INHERITANCE: Tightly Coupled
class Bird:
def fly(self):
return "Flying with wings"
class Penguin(Bird):
# ERROR: Penguins cannot fly!
# We are forced to override or break the contract.
def fly(self):
raise Exception("Penguins cannot fly")
# ✅ COMPOSITION: Flexible & Modular
class Flyable:
def fly(self):
return "Flying with wings"
class Swimmable:
def swim(self):
return "Swimming with flippers"
class Penguin:
def __init__(self):
# Composition allows us to pick only what we need
self.swimmer = Swimmable()
def swim(self):
return self.swimmer.swim()
The "Hot Swap" Advantage
Imagine you are building a game engine. You want to swap a standard engine for a turbo engine at runtime. With inheritance, you'd need a new class TurboCar. With composition, you just swap the object reference.
(In a real app, clicking "Swap" would trigger Anime.js to transition the engine above)
Key Takeaways
- Is-A vs Has-A: Use inheritance only if the child is a strict subtype of the parent. Otherwise, use composition.
- Interface Segregation: Compose objects that implement specific interfaces (like
SwimmableorFlyable) rather than monolithic base classes. - Runtime Flexibility: Composition allows you to change behavior at runtime by swapping components, a technique essential for async programming and event-driven architectures.
Advanced Object Composition: Strategy and Decorator Patterns
Welcome to the next level of architectural design. Up until now, you've likely relied heavily on inheritance to build your systems. While inheritance is powerful, it creates rigid hierarchies that are difficult to maintain as your application grows. Today, we shift our paradigm to Composition. We will explore two of the most potent design patterns in the Gang of Four (GoF) catalog: the Strategy Pattern for swapping algorithms at runtime, and the Decorator Pattern for adding responsibilities dynamically without subclassing.
1. The Strategy Pattern: Swapping Algorithms at Runtime
Imagine you are building a payment processing system. You have Credit Cards, PayPal, and Bitcoin. If you use inheritance, you might create a `PaymentProcessor` class and extend it for each type. But what if you want to switch payment methods dynamically based on user preference or transaction amount? That is where the Strategy Pattern shines. It defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Visualizing Runtime Swapping
The diagram above illustrates the power of indirection. The Context doesn't care how the work is done, only that it is done. This decoupling is essential for scalable systems.
Python Implementation
# Strategy Interface class PaymentStrategy: def pay(self, amount): pass # Concrete Strategies class CreditCardPayment(PaymentStrategy): def __init__(self, card_number): self.card_number = card_number def pay(self, amount): print(f"Paying ${amount} using Credit Card ending in {self.card_number[<-4:]}") class CryptoPayment(PaymentStrategy): def __init__(self, wallet_address): self.wallet_address = wallet_address def pay(self, amount): print(f"Paying ${amount} using Crypto from {self.wallet_address[<:8]}...") # Context class ShoppingCart: def __init__(self): self.items = [] self.payment_strategy = None def set_payment(self, strategy): self.payment_strategy = strategy def checkout(self): total = sum(self.items) if self.payment_strategy: self.payment_strategy.pay(total) else: print("No payment method selected.") # Usage cart = ShoppingCart() cart.items = [100, 50, 25] # Client decides strategy at runtime cart.set_payment(CreditCardPayment("1234-5678-9012-3456")) cart.checkout() # Switch strategy dynamically cart.set_payment(CryptoPayment("0x71C7656EC7ab88b098defB751B7401B5f6d8976F")) cart.checkout() 2. The Decorator Pattern: The "Russian Doll" Architecture
Sometimes, you need to add functionality to an object without modifying its code. Inheritance forces you to create a new subclass for every combination of features (e.g., CompressedEncryptedFile, EncryptedCompressedFile). This leads to class explosion. The Decorator Pattern solves this by wrapping the original object with new behavior, layer by layer.
Class Structure of a Decorator
Notice how CompressionDecorator and EncryptionDecorator both implement the DataSource interface. This allows you to stack them: new EncryptionDecorator(new CompressionDecorator(new FileDataSource())). This concept is similar to how RAII manages resources in C++, ensuring cleanup happens in reverse order of construction.
Python Implementation
import base64 import zlib class DataSource: def write_data(self, data): pass def read_data(self): pass class FileDataSource(DataSource): def __init__(self, filename): self.filename = filename self.data = "" def write_data(self, data): self.data = data print(f"Writing raw data to {self.filename}") def read_data(self): return self.data # The Decorator Base class DataSourceDecorator(DataSource): def __init__(self, source): self.source = source def write_data(self, data): self.source.write_data(data) def read_data(self): return self.source.read_data() # Concrete Decorators class CompressionDecorator(DataSourceDecorator): def write_data(self, data): compressed = zlib.compress(data.encode()) print("Compressing data...") super().write_data(compressed) def read_data(self): compressed = super().read_data() if isinstance(compressed, bytes): return zlib.decompress(compressed).decode() return compressed class EncryptionDecorator(DataSourceDecorator): def write_data(self, data): # Simple Base64 encoding for demo (not real encryption) encoded = base64.b64encode(data.encode()).decode() print("Encrypting data...") super().write_data(encoded) def read_data(self): encoded = super().read_data() if isinstance(encoded, str): return base64.b64decode(encoded).decode() return encoded # Usage: Stacking Decorators source = FileDataSource("report.txt") source = CompressionDecorator(source) source = EncryptionDecorator(source) source.write_data("Secret Data") print(f"Read back: {source.read_data()}") Key Takeaways for the Senior Architect
- Strategy Pattern: Use this when you have multiple algorithms for a specific task and want to switch between them at runtime. It adheres to the composition over inheritance principle.
- Decorator Pattern: Use this to add responsibilities to objects dynamically. It is superior to subclassing when you need to combine features in various ways (e.g., logging + compression + encryption).
- Interface Consistency: Both patterns rely heavily on interfaces. Ensure your decorators and strategies implement the same interface as the component they wrap or replace.
- Security Context: While the example used Base64, in production, you would use robust libraries for encryption, similar to how you would securely hash passwords using bcrypt or Argon2.
In the architecture of complex systems, code is only half the battle. The other half is time and memory. As a Senior Architect, I cannot stress this enough: objects are not free. Every instance you create carries a cost, and every dependency you inject adds a layer of coupling.
Mastering object lifecycles isn't just about preventing memory leaks; it's about designing systems that are resilient, predictable, and efficient. Whether you are managing a database connection pool or orchestrating microservices, understanding the birth, life, and death of your objects is the hallmark of a true professional.
The Lifecycle State Machine
Visualizing the journey of an object from allocation to garbage collection (or destruction).
Resource Acquisition Is Initialization (RAII)
In C++ and Rust, we rely on deterministic destruction. The concept of RAII binds the lifecycle of a resource (like a file handle or mutex) to the lifetime of an object. When the object goes out of scope, the resource is automatically released. This is the gold standard for preventing resource leaks in high-performance systems.
The RAII Pattern (C++)
Notice how the destructor ~DatabaseConnection() is guaranteed to run when the scope_guard leaves its block, regardless of how it exits (normal return or exception).
#include <iostream> #include <memory> class DatabaseConnection { public: DatabaseConnection() { std::cout << "Connecting to DB..." << std::endl; } ~DatabaseConnection() { std::cout << "Closing DB connection." << std::endl; } void query() { std::cout << "Executing query..." << std::endl; } }; void executeQuery() { // Smart pointer ensures destruction std::unique_ptr<DatabaseConnection> conn = std::make_unique<DatabaseConnection>(); conn->query(); // Scope ends here, destructor called automatically } int main() { executeQuery(); std::cout << "Main continues..." << std::endl; return 0; }
For a deeper dive into this specific safety mechanism, you should study how to use RAII for safe resource management.
Garbage Collection & The Cost of Abstraction
In managed environments like Java, C#, or Python, the Garbage Collector (GC) handles memory reclamation. While this reduces developer burden, it introduces non-deterministic pauses. The complexity of the GC algorithm often scales with the number of live objects, typically approaching $O(n)$ or $O(n \log n)$ depending on the collector strategy (e.g., Generational vs. Mark-and-Sweep).
⚠️ The "Stop-the-World" Phenomenon
When the GC runs, it often pauses all application threads to safely analyze the heap. In high-frequency trading or real-time robotics, this latency is unacceptable.
Managing Dependencies: The Inversion of Control
Objects rarely exist in isolation. They depend on other objects to function. Hard-coding these dependencies creates tight coupling, making your system brittle. Dependency Injection (DI) solves this by pushing dependencies into an object from the outside.
The Dependency Graph
Visualizing how a high-level module depends on abstractions (interfaces), not concrete implementations.
This architectural pattern is critical when you need to test your code. By injecting a Mock Database, you can test your logic without touching a real server. This concept is foundational when you learn how to build concurrent applications, where shared resources must be managed carefully.
Key Takeaways
- Deterministic vs. Non-Deterministic: Understand the difference between RAII (C++) and Garbage Collection (Java/Python). RAII offers immediate cleanup; GC offers convenience but potential latency.
- Scope is King: Always limit the scope of your variables. The shorter the lifetime, the easier it is for the system to reclaim memory.
-
Dependency Injection: Never
newa dependency inside a class. Inject it via constructor or setter to maintain flexibility and testability.
Best Practices and Common Pitfalls in OOP Design
Welcome to the architecture layer. You have learned the syntax of classes and objects, but now we must address the art of design. Writing code that compiles is easy; writing code that scales, survives refactoring, and remains readable is the mark of a true Senior Engineer.
Object-Oriented Programming (OOP) is a double-edged sword. Used correctly, it creates modular, robust systems. Used poorly, it creates "spaghetti code" wrapped in class definitions. In this masterclass, we will dissect the most common architectural anti-patterns and replace them with industry-standard best practices.
The Cost of Tight Coupling
As you add dependencies between classes, the complexity of the system doesn't grow linearly—it grows exponentially. This is often represented by the Connectivity Complexity metric:
Where n is the number of tightly coupled components. Notice how adding just one more class to a tightly coupled system drastically increases the number of potential failure points. This is why we strive for Composition over Inheritance.
The Interactive Anti-Pattern Checklist
Before you write your next class, run it through this mental model. These are the most common reasons systems fail in production.
⚠ The "God Object" Syndrome
The Problem: A single class that knows too much or does too much. It handles database connections, UI rendering, and business logic all at once.
The Fix: Apply the Single Responsibility Principle (SRP). Break the God Object into smaller, focused services.
⚠ Anemic Domain Model
The Problem: Classes that are just data containers (Getters/Setters) with no behavior. The logic lives in external "Service" classes, turning your OOP code into procedural spaghetti.
The Fix: Put the logic inside the object. If a User object has a changePassword() method, the validation logic should be inside User, not in a UserManager.
⚠ Deep Inheritance Trees
The Problem: Creating hierarchies like Animal -> Mammal -> Primate -> Human -> Developer. This creates the "Brittle Base Class" problem where changing the top affects everyone below.
The Fix: Favor Composition. Instead of inheriting behavior, give the object a component that provides it.
Visualizing Coupling
Compare the "Spaghetti" approach (Left) with the "Modular" approach (Right).
Code Review: Refactoring for Safety
Let's look at a concrete example. We will refactor a class that violates the RAII (Resource Acquisition Is Initialization) principle by leaking file handles.
Key Takeaways
- Encapsulate State: Never expose internal data structures directly. Use getters/setters or properties to control access.
- Dependency Injection: Don't
newdependencies inside your class. Inject them. This makes testing significantly easier. - Fail Fast: Validate inputs at the boundary of your object. If an object is in an invalid state, it should refuse to be created.
Frequently Asked Questions
What is the difference between composition and inheritance in OOP?
Inheritance establishes an 'is-a' relationship where a child class inherits behavior from a parent. Composition establishes a 'has-a' relationship where a class contains instances of other classes to build functionality, offering greater flexibility.
When should I use composition over inheritance?
Use composition when you need to change behavior dynamically at runtime, when classes share functionality but not identity, or when you want to avoid tight coupling and deep inheritance hierarchies.
How do I implement composition in Python?
Define a class that accepts another class instance as an argument in its __init__ method and stores it as an attribute. This creates a dependency where the parent object uses the functionality of the child object.
What is a has-a relationship in object-oriented programming?
A has-a relationship means one object contains a reference to another object. For example, a Car has an Engine. This is the fundamental basis of object composition.
Is composition always better than inheritance?
Not always, but generally yes for flexibility. Inheritance is useful for polymorphism and shared interfaces, but composition is preferred for code reuse and reducing coupling in complex systems.
Can you give a real-world OOP composition example?
A Computer system is a classic example. A Computer has a CPU, RAM, and Hard Drive. These parts are separate objects composed together to form the Computer, rather than the Computer inheriting from CPU or RAM.
How does composition help with code reuse?
Composition allows you to reuse existing classes by including them as components in new classes without modifying their source code, adhering to the Open/Closed Principle.