Is-a vs Has-a¶
Relationship Types¶
1. Fundamental Concepts¶
In object-oriented design, relationships between classes fall into two primary categories:
- Is-a: Inheritance relationship (subclass/superclass)
- Has-a: Composition/Aggregation relationship (container/component)
Understanding these relationships is the foundation for every design decision in this chapter. The pages on Composition, Aggregation, Composition vs Inheritance, and Design Guidelines all build on the concepts introduced here.
Quick Rule
Default to composition. Use inheritance only when there is a true type relationship. Use aggregation when objects must be shared or outlive their container.
2. Identifying Relationships¶
Ask these questions:
- Is-a: "Is the subclass a specialized type of the superclass?"
- Has-a: "Does the class contain or use instances of another class?"
The answer guides your design choice.
3. Language Test¶
Use natural language to test relationships:
- Dog is a Animal ✅ (inheritance)
- Car has a Engine ✅ (composition)
- Student has a Course ✅ (aggregation)
Is-a Relationship¶
1. Inheritance Model¶
The is-a relationship represents a hierarchical connection where the subclass is a specialized version of the superclass:
```python class Animal: def init(self, name): self.name = name
def speak(self):
pass
class Dog(Animal): def speak(self): return "Woof!"
class Cat(Animal): def speak(self): return "Meow!" ```
2. Key Characteristics¶
- Subclass inherits all attributes and methods
- Subclass can override inherited behavior
- Represents generalization/specialization
- Creates a tight coupling between classes
3. When to Use¶
Use inheritance when:
```python
Clear specialization hierarchy¶
class Vehicle: pass
class Car(Vehicle): # Car is-a Vehicle ✅ pass
class Truck(Vehicle): # Truck is-a Vehicle ✅ pass
Polymorphic behavior needed¶
def process_vehicle(vehicle: Vehicle): vehicle.start() # Works for any Vehicle subclass ```
Has-a Relationship¶
1. Composition Model¶
The has-a relationship represents containment where one class holds references to instances of other classes:
```python class Engine: def start(self): return "Engine started"
class Car: def init(self): self.engine = Engine() # Car has-a Engine
def start(self):
return self.engine.start()
```
2. Key Characteristics¶
- Container class contains component objects
- Component lifetime depends on container (composition)
- Component lifetime independent of container (aggregation)
- Creates loose coupling between classes
3. When to Use¶
Use composition/aggregation when:
```python
Building from components¶
class Computer: def init(self): self.cpu = CPU() self.ram = RAM() self.storage = Storage()
Flexible assembly¶
class Team: def init(self, players): self.players = players # Team has-a Players ```
Comparison¶
1. Coupling Differences¶
| Aspect | Is-a (Inheritance) | Has-a (Composition) |
|---|---|---|
| Coupling | Tight | Loose |
| Flexibility | Less | More |
| Reusability | Limited | High |
| Change impact | High | Low |
2. Code Examples¶
Is-a (Inheritance): ```python class Shape: def area(self): pass
class Circle(Shape): def init(self, radius): self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
```
Has-a (Composition): ```python class Point: def init(self, x, y): self.x = x self.y = y
class Circle: def init(self, center, radius): self.center = center # Has-a Point self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
```
3. When Each Fails¶
Inheritance fails when: ```python
❌ BAD: Square is-a Rectangle (violates LSP)¶
class Rectangle: def set_width(self, w): self.width = w def set_height(self, h): self.height = h
class Square(Rectangle): # Must keep width == height, but set_width/set_height # allow them to differ → invariant violated. # Code expecting a Rectangle (where width and height # can change independently) breaks with a Square. pass ```
This is the classic Liskov Substitution trap: mathematically a square is a rectangle, but in code Square cannot honor Rectangle's contract that width and height are independently settable.
Composition works: ```python
✅ GOOD: Square has-a Shape interface¶
class Square: def init(self, side): self.side = side
def area(self):
return self.side ** 2
```
Unified Mental Model¶
All OOP relationships can be understood through four dimensions:
| Dimension | Inheritance (is-a) | Composition (has-a, strong) | Aggregation (has-a, weak) |
|---|---|---|---|
| Ownership | Implicit (type system) | Container owns parts | No ownership |
| Lifetime | Tied to type | Parts die with container | Independent |
| Coupling | Tight | Moderate | Loose |
| Flexibility | Low (compile-time) | Medium | High (runtime) |
When choosing, think about ownership (who creates/destroys?), lifetime (do parts outlive the container?), coupling (how much does one class know about another?), and flexibility (can behavior change at runtime?).
Python Reality
In Python, the composition vs aggregation distinction is conceptual, not enforced. There is no language-level mechanism that ties an object's lifetime to its container or prevents sharing. The difference is purely design intent — whether the container creates its parts internally (composition) or receives pre-existing objects from outside (aggregation). Python's garbage collector handles lifetime automatically based on reference counts, regardless of which pattern you intended.
Design Guidelines¶
1. Favor Composition¶
Modern OOP design favors composition over inheritance:
```python
Instead of deep inheritance¶
class Animal: pass
class Mammal(Animal): pass
class Dog(Mammal): pass
Use composition¶
class Animal: def init(self, behavior): self.behavior = behavior # Has-a Behavior
def act(self):
self.behavior.perform()
```
2. Mix Both Approaches¶
Combine inheritance and composition:
```python class Drawable: """Interface through inheritance""" def draw(self): pass
class Circle(Drawable): def init(self, center, radius): self.center = center # Has-a Point self.radius = radius
def draw(self):
# Implementation
pass
```
3. Decision Tree¶
Need to model relationship?
↓
Is it specialization? → Use Inheritance (is-a)
↓
Is it containment? → Use Composition (has-a)
↓
Strong ownership? → Composition
↓
Weak ownership? → Aggregation
Common Patterns¶
1. Interface Inheritance¶
Use inheritance for interfaces, composition for implementation:
```python from abc import ABC, abstractmethod
class PaymentProcessor(ABC): @abstractmethod def process(self, amount): pass
class CreditCardProcessor(PaymentProcessor): def init(self, gateway): self.gateway = gateway # Has-a Gateway
def process(self, amount):
return self.gateway.charge(amount)
```
2. Strategy Pattern¶
Combine both:
```python class SortStrategy(ABC): @abstractmethod def sort(self, data): pass
class QuickSort(SortStrategy): def sort(self, data): # Quick sort implementation pass
class DataProcessor: def init(self, strategy): self.strategy = strategy # Has-a Strategy
def process(self, data):
return self.strategy.sort(data)
```
3. Decorator Pattern¶
Composition for extending behavior:
```python class Coffee: def cost(self): return 5
class MilkDecorator: def init(self, coffee): self.coffee = coffee # Has-a Coffee
def cost(self):
return self.coffee.cost() + 2
coffee = MilkDecorator(Coffee()) print(coffee.cost()) # 7 ```
Real-World Examples¶
1. UI Components¶
```python
Inheritance for type hierarchy¶
class Widget: pass
class Button(Widget): pass
class TextField(Widget): pass
Composition for assembly¶
class Form: def init(self): self.buttons = [] # Has-a Buttons self.textfields = [] # Has-a TextFields ```
2. Game Entities¶
```python
Inheritance for base entity¶
class GameObject: pass
class Character(GameObject): def init(self): self.position = Vector2D() # Has-a Position self.health = HealthBar() # Has-a HealthBar self.inventory = [] # Has-a Items ```
3. Business Systems¶
```python
Inheritance for business entities¶
class Person: pass
class Employee(Person): def init(self, department): self.department = department # Has-a Department
class Customer(Person): def init(self, orders): self.orders = orders # Has-a Orders ```
Testing Validity¶
1. Liskov Substitution¶
Test inheritance with LSP:
```python def process_animal(animal: Animal): animal.speak()
Should work for any subclass¶
process_animal(Dog()) process_animal(Cat()) ```
2. Composition Validity¶
Test composition by swapping components:
```python class Car: def init(self, engine): self.engine = engine
Should work with any Engine¶
car1 = Car(ElectricEngine()) car2 = Car(GasEngine()) ```
3. Relationship Questions¶
Ask yourself:
- Can I say "B is-a A" naturally? → Inheritance
- Can I say "B has-a A" naturally? → Composition
- Does B exist without A? → Aggregation
- Is B meaningless without A? → Composition
Exercises¶
Exercise 1.
For each of the following pairs, decide whether the relationship is "is-a" (inheritance) or "has-a" (composition/aggregation), and explain why: (a) Dog and Animal, (b) Car and Engine, (c) Manager and Employee, (d) Library and Book.
Solution to Exercise 1
# (a) Dog is-a Animal — inheritance
# A dog IS a kind of animal. Natural type hierarchy.
# (b) Car has-a Engine — composition
# A car is NOT an engine. It contains an engine as a part.
# (c) Manager is-a Employee — inheritance
# A manager IS a specialized type of employee.
# (d) Library has-a Book — aggregation
# A library contains books, but books exist independently.
# Books can be moved between libraries.
Exercise 2.
Model a University system. A University has-a list of Department objects (composition---departments don't exist without the university). Each Department has-a list of Professor objects (aggregation---professors exist independently). Implement both relationships and demonstrate the lifecycle differences.
Solution to Exercise 2
class Professor:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Prof({self.name})"
class Department:
def __init__(self, name, professors):
self.name = name
self.professors = professors # Aggregation
class University:
def __init__(self, name, dept_names):
# Composition: departments created here
self.name = name
self._departments = [Department(n, []) for n in dept_names]
def get_department(self, name):
for d in self._departments:
if d.name == name:
return d
return None
prof_a = Professor("Dr. Smith")
prof_b = Professor("Dr. Jones")
uni = University("MIT", ["CS", "Math"])
cs = uni.get_department("CS")
cs.professors.append(prof_a)
cs.professors.append(prof_b)
del uni # Departments destroyed (composition)
print(prof_a) # Prof(Dr. Smith) — still exists (aggregation)
Exercise 3.
Create an Employee class with name and role. Then create Company that owns a list of employees (composition) and Project that references employees (aggregation). Show that when a Company is deleted, its employees are gone (if no other references exist), but employees assigned to a Project from an external source survive the project's deletion.
Solution to Exercise 3
class Employee:
def __init__(self, name, role):
self.name = name
self.role = role
def __repr__(self):
return f"Employee('{self.name}')"
class Company:
def __init__(self, name):
self.name = name
self._employees = [] # Composition: owns employees
def hire(self, name, role):
emp = Employee(name, role)
self._employees.append(emp)
return emp
class Project:
def __init__(self, name, members):
self.name = name
self.members = members # Aggregation: references
company = Company("Acme")
alice = company.hire("Alice", "Developer")
bob = company.hire("Bob", "Designer")
# External reference to alice for the project
project = Project("Website", [alice])
del project
print(alice) # Employee('Alice') — survives project deletion
Exercise 4.
The classic Square/Rectangle problem illustrates when "is-a" thinking goes wrong. Implement a Rectangle with set_width and set_height methods, and a Square that inherits from it. Show a function that expects a Rectangle (sets width and height independently) but breaks when given a Square. Explain the Liskov Substitution Principle violation.
Solution to Exercise 4
class Rectangle:
def __init__(self, w, h):
self._w = w
self._h = h
def set_width(self, w):
self._w = w
def set_height(self, h):
self._h = h
def area(self):
return self._w * self._h
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, w):
self._w = w
self._h = w # Must keep square invariant
def set_height(self, h):
self._w = h
self._h = h
def double_width(rect: Rectangle):
"""Assumes width and height are independent."""
old_height = rect._h
rect.set_width(rect._w * 2)
assert rect._h == old_height, "Height should not change!"
return rect.area()
r = Rectangle(4, 5)
print(double_width(r)) # 40 — works fine
s = Square(4)
try:
double_width(s) # AssertionError: Height should not change!
except AssertionError as e:
print(f"LSP violation: {e}")
LSP violation: Rectangle's contract says width and height can change independently. Square breaks this contract — setting the width also changes the height. Any code that depends on independent dimensions breaks when a Square is substituted for a Rectangle. Mathematically a square is a rectangle, but in code the Square class cannot satisfy the Rectangle interface contract.
Fix: don't inherit Square from Rectangle. Either make both independent classes implementing a Shape interface, or use a single Rectangle class with a factory method Rectangle.square(side).
Exercise 5. For each scenario below, decide whether to use inheritance, composition, or aggregation. Justify your choice using the four dimensions from the Unified Mental Model table (ownership, lifetime, coupling, flexibility).
- A
Browserthat uses aRenderingEngine - A
Studentenrolled in multipleCourses - A
CheckingAccountin a banking system that is a kind ofBankAccount
Solution to Exercise 5
1. Browser / RenderingEngine → Composition
- Ownership: The browser creates and owns its rendering engine.
- Lifetime: The engine lives and dies with the browser.
- Coupling: Moderate — the browser delegates rendering to the engine.
- Flexibility: The engine could be swapped (e.g., switching from one rendering backend to another).
A browser has-a rendering engine; it is not a rendering engine.
2. Student / Courses → Aggregation
- Ownership: Neither owns the other. A course exists before students enroll.
- Lifetime: Students and courses have independent lifetimes.
- Coupling: Loose — a student holds references to courses.
- Flexibility: High — students can enroll/unenroll at runtime, and courses can be shared across students.
A student has courses, but does not own them.
3. CheckingAccount / BankAccount → Inheritance
- Ownership: Type system relationship — no part/container involved.
- Lifetime: The subclass is the superclass (same object).
- Coupling: Tight, but appropriate — all bank accounts share a core interface.
- Flexibility: Low, but the hierarchy is stable (account types don't change often).
A checking account is-a bank account. The hierarchy is shallow and stable, and polymorphism is needed (e.g., processing a list of mixed account types).