By the end of this chapter, you should understand:
- What composition means in object-oriented programming.
- How objects can be built from other objects.
- The difference between "has-a" and "is-a" relationships.
- Why composition is often simpler than inheritance.
- How encapsulation and composition work together.
- How delegation works.
- How composed objects affect ownership and lifetime.
- How dependency injection supports composition.
- How composition improves testing.
- How to avoid over-composition.
- How composition prepares us to evaluate inheritance properly.
Chapter 45 focused on object boundaries.
Composition focuses on object relationships.
Encapsulation asks:
What should this object expose?
What should it keep internal?
Composition asks:
What other objects should this object use to do its job?
This is one of the most important design ideas in object-oriented programming.
Before we study inheritance, we need composition.
Why?
Because many problems that beginners try to solve with inheritance are better solved by connecting objects together.
Composition means building an object out of other objects.
Example:
class Engine:
def start(self):
print("engine starting")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()Use:
car = Car()
car.start()Output:
engine starting
The Car object contains an Engine object.
Relationship:
Car has an Engine
This is composition.
The car is not an engine.
The car has an engine.
That distinction matters.
Two common object relationships:
has-a
is-a
Composition models has-a relationships.
Inheritance models is-a relationships.
Examples:
Car has an Engine.
House has Rooms.
Order has LineItems.
Playlist has Songs.
Report has a Formatter.
Service has a Repository.
Inheritance examples:
Dog is an Animal.
SavingsAccount is an Account.
CsvReport is a Report.
Beginners often overuse inheritance because it feels like the main object-oriented tool.
But in real Python design, composition is often the first tool to consider.
Question:
Is this object a kind of that object?
Or does this object use that object?
If it uses or owns another object, composition is probably the better model.
Composition gives programs flexible structure.
Example:
class EmailSender:
def send(self, message):
print("email:", message)
class NotificationService:
def __init__(self):
self.sender = EmailSender()
def notify(self, message):
self.sender.send(message)The notification service uses a sender.
Later, we can change the sender:
class SmsSender:
def send(self, message):
print("sms:", message)If NotificationService is designed well, it can use either sender.
Composition lets behavior vary by swapping collaborator objects.
This is powerful.
Instead of building one huge class that does everything, we can build small objects that cooperate.
In composition, objects collaborate.
Example:
class Formatter:
def format(self, task):
marker = "x" if task["done"] else " "
return f"[{marker}] {task['title']}"
class TaskPrinter:
def __init__(self, formatter):
self.formatter = formatter
def print_task(self, task):
print(self.formatter.format(task))Use:
formatter = Formatter()
printer = TaskPrinter(formatter)
task = {"title": "learn composition", "done": False}
printer.print_task(task)Output:
[ ] learn composition
The TaskPrinter does not know formatting details.
It delegates formatting to Formatter.
Relationship:
TaskPrinter has a Formatter
TaskPrinter uses Formatter to do part of its job
This keeps responsibilities separate.
Composition works best with encapsulation.
Example:
class Engine:
def start(self):
print("engine starting")
class Car:
def __init__(self):
self._engine = Engine()
def start(self):
self._engine.start()The car exposes:
car.start()It keeps internal:
car._engineCallers do not need to know exactly how the car starts.
Maybe today it uses an Engine.
Tomorrow it might use an ElectricMotor.
If the public interface stays:
car.start()callers do not need to change.
Encapsulation defines the boundary.
Composition fills the inside of that boundary with collaborating objects.
Direct composition means an object creates the object it uses.
Example:
class FileStorage:
def save(self, text):
print("saving to file:", text)
class NotesApp:
def __init__(self):
self._storage = FileStorage()
def save_note(self, text):
self._storage.save(text)Use:
app = NotesApp()
app.save_note("learn Python")This is simple.
NotesApp owns its storage object.
Direct composition is fine when:
- The dependency is simple.
- There is only one sensible implementation.
- You do not need to swap it in tests.
- The owning object should control creation.
But direct composition can become rigid.
If NotesApp must support file storage, database storage, and memory storage, we need more flexibility.
That leads to dependency injection.
Dependency injection means giving an object its dependencies from the outside.
Instead of:
class NotesApp:
def __init__(self):
self._storage = FileStorage()write:
class NotesApp:
def __init__(self, storage):
self._storage = storage
def save_note(self, text):
self._storage.save(text)Use:
storage = FileStorage()
app = NotesApp(storage)Now NotesApp does not care which storage object it receives, as long as it supports:
save(text)This is composition with external wiring.
Dependency injection improves:
- Flexibility.
- Testability.
- Separation of responsibilities.
- Reuse.
The object receives collaborators instead of creating all of them internally.
With dependency injection, we can swap collaborators.
Example:
class FileStorage:
def save(self, text):
print("file:", text)
class MemoryStorage:
def __init__(self):
self.items = []
def save(self, text):
self.items.append(text)Application:
class NotesApp:
def __init__(self, storage):
self._storage = storage
def save_note(self, text):
self._storage.save(text)Use file storage:
app = NotesApp(FileStorage())
app.save_note("hello")Use memory storage:
memory = MemoryStorage()
app = NotesApp(memory)
app.save_note("hello")
print(memory.items)Output:
['hello']
NotesApp did not change.
Only the collaborator changed.
That is composition doing real design work.
Delegation means one object passes part of its work to another object.
Example:
class JsonFormatter:
def format(self, data):
return str(data)
class Report:
def __init__(self, formatter):
self._formatter = formatter
def render(self):
data = {"status": "ok"}
return self._formatter.format(data)The report handles report logic.
The formatter handles formatting.
Report.render() delegates formatting to:
self._formatter.format(data)Delegation is common in composed designs.
It lets one class stay focused.
Instead of one object doing everything, each collaborator handles its own responsibility.
Core idea:
I know when formatting is needed.
My formatter knows how formatting works.
That division is clean.
Composition raises an ownership question:
Who owns the collaborator object?
Example:
class Car:
def __init__(self):
self._engine = Engine()The car creates the engine.
It likely owns it.
If the car disappears, the engine likely disappears too.
But:
engine = Engine()
car = Car(engine)Now the engine was created outside.
Maybe the car uses it but does not fully own it.
Ownership affects:
- Object lifetime.
- Cleanup responsibility.
- Mutation responsibility.
- Sharing.
- Testing.
Ask:
Does this object create the dependency?
Can other objects share the dependency?
Who is responsible for closing or cleaning it up?
Composition is not only structure.
It is ownership design.
Object relationships affect lifetime.
Example:
class Service:
def __init__(self, database):
self._database = databaseAs long as the Service object is alive, it keeps a strong reference to the database object.
Conceptually:
service ──▶ database
If no other references exist, the database still stays alive through the service.
This is ordinary reference behavior from Volume I.
Composition creates object graphs.
Those graphs affect memory and cleanup.
If the collaborator manages an external resource, such as a file or socket, lifetime matters even more.
The owner must know who closes it.
Good composition makes ownership clear.
Bad composition leaves cleanup ambiguous.
Sometimes many objects share one collaborator.
Example:
class Logger:
def log(self, message):
print(message)
class UserService:
def __init__(self, logger):
self._logger = logger
class OrderService:
def __init__(self, logger):
self._logger = loggerUse:
logger = Logger()
users = UserService(logger)
orders = OrderService(logger)Graph:
users ──▶ logger
orders ──▶ logger
This can be good.
One logger may be shared across the application.
But shared mutable collaborators need care.
If many objects mutate the same collaborator, behavior can become hard to reason about.
Ask:
Is sharing intentional?
Is the collaborator safe to share?
Does it hold mutable state?
Who configures it?
Inheritance says:
this class is a kind of another class
Composition says:
this class uses another object
Suppose we need a report that can output JSON.
Inheritance approach:
class JsonReport(Report):
...Composition approach:
class Report:
def __init__(self, formatter):
self._formatter = formatterIf formatting is a behavior that can vary, composition is often cleaner.
You can create:
Report(JsonFormatter())
Report(HtmlFormatter())
Report(TextFormatter())without building a subclass for every format.
Inheritance can be powerful.
But composition often gives flexibility with less hierarchy.
Before using inheritance, ask:
Is this truly an is-a relationship?
Or am I just trying to reuse behavior?
Composition reuses behavior by holding an object that provides that behavior.
Example:
class Slugifier:
def slugify(self, text):
return text.lower().replace(" ", "-")
class ArticleService:
def __init__(self, slugifier):
self._slugifier = slugifier
def create_slug(self, title):
return self._slugifier.slugify(title)Use:
service = ArticleService(Slugifier())
print(service.create_slug("Hello World"))Output:
hello-world
ArticleService reuses slugification behavior.
It does not need to inherit from Slugifier.
This is often better than:
class ArticleService(Slugifier):
...because an article service is not a kind of slugifier.
It uses a slugifier.
That is composition.
Bad inheritance:
class EmailSender:
def send_email(self, message):
print("email:", message)
class UserService(EmailSender):
def welcome_user(self, user):
self.send_email(f"Welcome {user}")Problem:
UserService is not an EmailSender
UserService uses email sending
Better composition:
class UserService:
def __init__(self, email_sender):
self._email_sender = email_sender
def welcome_user(self, user):
self._email_sender.send_email(f"Welcome {user}")Now the relationship is accurate:
UserService has an EmailSender
This avoids confusing type relationships.
Inheritance should model meaning, not just convenience.
Composition makes testing easier.
Example:
class EmailSender:
def send(self, message):
print("sending real email:", message)
class UserService:
def __init__(self, sender):
self._sender = sender
def welcome(self, name):
self._sender.send(f"Welcome {name}")In production:
service = UserService(EmailSender())In tests:
class FakeSender:
def __init__(self):
self.messages = []
def send(self, message):
self.messages.append(message)
fake = FakeSender()
service = UserService(fake)
service.welcome("Ada")
assert fake.messages == ["Welcome Ada"]No real email is sent.
The service is easy to test because the sender is injected.
Composition helps separate business behavior from external effects.
A fake object is a simple replacement used in tests.
Example:
class FakeStorage:
def __init__(self):
self.saved = []
def save(self, value):
self.saved.append(value)Use:
storage = FakeStorage()
app = NotesApp(storage)
app.save_note("test")
assert storage.saved == ["test"]The fake has the same method the app needs:
save(value)It does not need to be a subclass of real storage.
It only needs to behave the same for the required operation.
This connects to duck typing, which we will study later.
Composition and duck typing work beautifully together.
The object cares about what the collaborator can do, not necessarily its exact class.
When using composition, you often depend on behavior.
Example:
class NotesApp:
def __init__(self, storage):
self._storage = storage
def save_note(self, text):
self._storage.save(text)NotesApp expects:
storage has a save(text) method
That expectation is a protocol in the informal sense.
The storage object can be:
FileStorageMemoryStorageDatabaseStorageFakeStorage
as long as it supports:
save(text)Later, we will study formal protocols and static typing.
For now, this is enough:
composition often depends on expected behavior, not exact class
Sometimes an object exposes a method that simply delegates.
Example:
class Engine:
def start(self):
print("engine started")
class Car:
def __init__(self, engine):
self._engine = engine
def start(self):
self._engine.start()Why not expose:
car.engine.start()Maybe because the car wants to keep the engine internal.
Public API:
car.start()Internal collaborator:
car._engineThe car controls what starting means.
Maybe later:
def start(self):
self._battery.check()
self._engine.start()
self._dashboard.show_ready()Callers still use:
car.start()Delegation preserves encapsulation.
Delegation does not mean doing nothing except forwarding.
Example:
class Storage:
def save(self, text):
print("saved:", text)
class NotesApp:
def __init__(self, storage):
self._storage = storage
def save_note(self, text):
text = text.strip()
if text == "":
raise ValueError("note cannot be empty")
self._storage.save(text)NotesApp handles note rules.
Storage handles saving.
This is better than putting every rule in storage or every storage detail in the app.
Good composition separates responsibilities:
NotesApp -> note validation and workflow
Storage -> persistence
Delegation often includes preparation, validation, transformation, or coordination.
Composition creates object graphs.
Example:
class App:
def __init__(self, users, orders):
self.users = users
self.orders = ordersConceptually:
app ──▶ user service ──▶ user repository ──▶ database
│
└──▶ order service ──▶ order repository ──▶ database
Object graphs matter because:
- They show dependencies.
- They show ownership.
- They show lifetimes.
- They show possible cycles.
- They show test boundaries.
In small examples, graphs are tiny.
In real applications, graphs can be large.
Good composition keeps the graph understandable.
Bad composition creates tangled dependency webs.
Composition can create cycles.
Example:
class Parent:
def __init__(self):
self.children = []
def add_child(self, child):
child.parent = self
self.children.append(child)Use:
parent = Parent()
child = Child()
parent.add_child(child)Graph:
parent ──▶ children list ──▶ child
▲ │
│ ▼
└──────── parent ◀────────┘
This cycle may be perfectly reasonable.
Python's garbage collector can handle many unreachable cycles.
But cycles affect ownership thinking.
If child should not keep parent alive, use a weak reference.
If parent owns child and child merely observes parent, weak references may fit.
Composition connects directly to memory management.
If a composed object owns a resource-owning object, cleanup matters.
Example:
class FileWriter:
def __init__(self, path):
self._file = open(path, "w")
def write(self, text):
self._file.write(text)
def close(self):
self._file.close()Composed owner:
class ReportWriter:
def __init__(self, file_writer):
self._file_writer = file_writer
def write_report(self, text):
self._file_writer.write(text)Who closes the file writer?
Possible answers:
- The creator closes it.
- The
ReportWriterowns it and closes it. - A context manager handles it.
The design must be clear.
Composition without cleanup responsibility can leak resources.
Ownership is not academic.
It affects correctness.
Context managers can express ownership and cleanup.
Example:
class ReportWriter:
def __init__(self, path):
self._path = path
self._file = None
def __enter__(self):
self._file = open(self._path, "w")
return self
def __exit__(self, exc_type, exc, tb):
self._file.close()
def write(self, text):
self._file.write(text)Use:
with ReportWriter("report.txt") as writer:
writer.write("hello")We will study context managers deeply later.
For now, see the design connection:
composition creates ownership
ownership may require cleanup
context managers express cleanup boundaries
Object design, memory management, and resource management are connected.
Composition is often used for configuration.
Example:
class HtmlFormatter:
def format(self, text):
return f"<p>{text}</p>"
class MarkdownFormatter:
def format(self, text):
return f"**{text}**"
class Renderer:
def __init__(self, formatter):
self._formatter = formatter
def render(self, text):
return self._formatter.format(text)Use:
html_renderer = Renderer(HtmlFormatter())
markdown_renderer = Renderer(MarkdownFormatter())Same Renderer class.
Different behavior through different collaborators.
Composition lets you configure behavior without creating many subclasses.
This is a major reason composition scales well.
Composition works best when classes have focused responsibilities.
Bad:
class Application:
def parse_input(self): ...
def validate_user(self): ...
def save_database(self): ...
def send_email(self): ...
def render_html(self): ...
def log_event(self): ...This class does too much.
Better:
Application
uses Parser
uses Validator
uses Repository
uses EmailSender
uses Renderer
uses Logger
Each collaborator can be understood and tested separately.
But do not overdo it.
One-method classes everywhere can make code fragmented.
Balance:
split responsibilities when it reduces real complexity
keep things together when they naturally belong together
Composition should make code easier to understand, not more ceremonious.
Sometimes a module is enough.
Example:
# formatting.py
def format_task(task):
marker = "x" if task.done else " "
return f"[{marker}] {task.title}"Using a class:
class TaskFormatter:
def format(self, task):
marker = "x" if task.done else " "
return f"[{marker}] {task.title}"Which is better?
It depends.
Use a module function when:
- There is no state.
- There is no need to swap implementations.
- There is no configuration.
- The behavior is simple.
Use a composed object when:
- It holds configuration.
- It has multiple related methods.
- You want to swap behavior.
- You want test fakes.
- It represents a meaningful collaborator.
Composition is powerful, but functions and modules are still excellent tools.
Sometimes a composed object wraps a data structure.
Example:
class History:
def __init__(self):
self._items = []
def record(self, item):
self._items.append(item)
def latest(self):
if not self._items:
return None
return self._items[-1]
@property
def items(self):
return tuple(self._items)The class uses a list internally.
Public interface:
history.record(item)
history.latest()
history.itemsInternal detail:
history._itemsThe list is composed inside the object.
This adds rules and meaning around a raw data structure.
Use this when the rules matter.
Do not wrap every list just because you can.
Composition helps create stable APIs.
Example:
class ReportService:
def __init__(self, repository, formatter):
self._repository = repository
self._formatter = formatter
def report_for_user(self, user_id):
data = self._repository.load_user_data(user_id)
return self._formatter.format(data)Public API:
service.report_for_user(user_id)Internal collaborators:
repository
formatterLater, you can replace:
DatabaseRepository with ApiRepository
JsonFormatter with HtmlFormatter
The public API can remain stable.
Composition lets internals vary behind an interface.
This is encapsulation at object-network scale.
Collaborators:
class EmailSender:
def send(self, recipient, message):
print(f"email to {recipient}: {message}")
class SmsSender:
def send(self, recipient, message):
print(f"sms to {recipient}: {message}")Service:
class NotificationService:
def __init__(self, sender):
self._sender = sender
def notify(self, user, message):
if user.get("disabled"):
return
self._sender.send(user["contact"], message)Use:
email_service = NotificationService(EmailSender())
sms_service = NotificationService(SmsSender())
user = {"contact": "ada@example.com", "disabled": False}
email_service.notify(user, "Welcome")Same service logic.
Different sending strategy.
Composition avoids:
EmailNotificationService
SmsNotificationService
PushNotificationService
unless those subclasses truly add meaningful type-specific behavior.
Classes:
class LineItem:
def __init__(self, name, quantity, price):
self.name = name
self.quantity = quantity
self.price = price
def total(self):
return self.quantity * self.price
class Order:
def __init__(self):
self._items = []
def add_item(self, item):
self._items.append(item)
def total(self):
amount = 0
for item in self._items:
amount += item.total()
return amountUse:
order = Order()
order.add_item(LineItem("Book", 2, 30))
order.add_item(LineItem("Pen", 3, 5))
print(order.total())Output:
75
Relationship:
Order has LineItems
The order delegates item subtotal calculation to each line item.
This is natural composition.
Formatters:
class TextFormatter:
def format(self, rows):
return "\n".join(rows)
class HtmlFormatter:
def format(self, rows):
items = "".join(f"<li>{row}</li>" for row in rows)
return f"<ul>{items}</ul>"Report:
class Report:
def __init__(self, rows, formatter):
self._rows = rows
self._formatter = formatter
def render(self):
return self._formatter.format(self._rows)Use:
rows = ["one", "two"]
text_report = Report(rows, TextFormatter())
html_report = Report(rows, HtmlFormatter())
print(text_report.render())
print(html_report.render())The report owns the workflow.
The formatter owns representation.
This is cleaner than putting every formatting style into one large Report class.
Repository:
class InMemoryUserRepository:
def __init__(self):
self._users = {}
def save(self, user_id, user):
self._users[user_id] = user
def get(self, user_id):
return self._users.get(user_id)Service:
class UserService:
def __init__(self, repository):
self._repository = repository
def register(self, user_id, name):
if name.strip() == "":
raise ValueError("name cannot be empty")
user = {"id": user_id, "name": name.strip()}
self._repository.save(user_id, user)
return user
def find(self, user_id):
return self._repository.get(user_id)Use:
repository = InMemoryUserRepository()
service = UserService(repository)
service.register(1, "Ada")
print(service.find(1))Composition separates:
UserService -> business rules
Repository -> storage
This is a common professional pattern.
Bad:
class Engine:
def start(self):
print("engine")
class Car(Engine):
passThis says:
Car is an Engine
But that is not true.
A car has an engine.
Better:
class Car:
def __init__(self, engine):
self._engine = engine
def start(self):
self._engine.start()Use inheritance for genuine is-a relationships.
Use composition for has-a relationships.
This simple distinction prevents many design problems.
Composition can go wrong if one object simply collects everything.
Example:
class AppManager:
def __init__(self):
self.users = UserService()
self.orders = OrderService()
self.emails = EmailSender()
self.reports = ReportGenerator()
self.cache = Cache()
self.logger = Logger()Maybe this is useful as an application container.
Maybe it is a sign that one class has become a global dumping ground.
Ask:
Does this object have a clear responsibility?
Or is it just holding unrelated things?
Composition should organize responsibilities.
It should not create a vague object that owns the whole world.
Over-composition can make code harder to follow.
Example:
UserNameNormalizer
UserNameTrimmer
UserNameTitleCaser
UserNameEmptyChecker
UserNameValidator
Maybe this is needed in a complex system.
Often, it is too much.
Simpler:
class UserNameValidator:
def normalize(self, name):
return name.strip().title()
def validate(self, name):
if name == "":
raise ValueError("name cannot be empty")Composition should reduce complexity.
If it multiplies files and objects without improving clarity, step back.
Design is balance.
Common Mistake: Hidden Construction
Rigid:
class UserService:
def __init__(self):
self._repository = DatabaseUserRepository()This hides the dependency.
It makes testing harder.
It forces every UserService to use the database repository.
More flexible:
class UserService:
def __init__(self, repository):
self._repository = repositoryNow the dependency is visible.
Production can pass:
DatabaseUserRepository()Tests can pass:
InMemoryUserRepository()Constructor arguments reveal what an object needs.
That is a good thing.
Suppose:
class Car:
def __init__(self, engine):
self.engine = engineNow callers may do:
car.engine.start()
car.engine.replace_part(...)
car.engine.internal_state = ...Maybe that is intended.
Maybe it leaks internals.
If callers should only start the car, expose:
class Car:
def __init__(self, engine):
self._engine = engine
def start(self):
self._engine.start()This keeps the engine internal.
Expose collaborators directly only when that is part of the public API.
Otherwise, delegate through clear methods.
Example:
file = open("report.txt", "w")
writer = ReportWriter(file)Who closes file?
The caller?
The writer?
Both?
Neither?
This ambiguity causes bugs.
Make ownership explicit.
Possible design:
with open("report.txt", "w") as file:
writer = ReportWriter(file)
writer.write("hello")Here the caller owns the file lifetime.
Another design:
with ReportWriter("report.txt") as writer:
writer.write("hello")Here ReportWriter owns the file lifetime.
Both can be valid.
The danger is not choosing.
Rigid:
class ReportService:
def __init__(self, formatter: HtmlFormatter):
self._formatter = formatterIf the service only needs:
formatter.format(data)then the exact class may not matter.
At runtime, Python only needs an object with the right behavior.
This allows:
ReportService(TextFormatter())
ReportService(HtmlFormatter())
ReportService(FakeFormatter())Static typing can express this later with protocols.
At the design level, think:
What behavior do I need from this collaborator?
not always:
What exact class must it be?
When using composition, ask:
What responsibility does this object have?
What collaborators does it need?
Does it create them or receive them?
Who owns each collaborator?
Can collaborators be shared?
Who cleans up resources?
Should a collaborator be public or internal?
Should this object delegate or expose the collaborator directly?
Would a function or module be simpler?
Would inheritance claim a false is-a relationship?
Will this design be easy to test?
General guidance:
- Use composition for has-a relationships.
- Prefer composition when behavior needs to vary.
- Inject dependencies when flexibility or testing matters.
- Keep collaborators focused.
- Do not expose internal collaborators unnecessarily.
- Make ownership and cleanup clear.
- Do not split objects so finely that the design becomes noisy.
- Use inheritance only when the relationship truly is-a.
Composition is not just a pattern.
It is how object-oriented programs are assembled.
- Create an
Engineclass and aCarclass.
Make Car use an Engine through composition.
Explain why Car should not inherit from Engine.
- Write a
NotesAppclass that receives astorageobject.
Create two storage classes:
FileStorage
MemoryStorage
Both should support:
save(text)Show that NotesApp works with either one.
- Write a fake storage object for testing.
Use it to test that NotesApp.save_note() saves the expected text.
Explain why composition makes this easy.
- Create a
Reportclass that receives a formatter.
Create:
TextFormatter
HtmlFormatter
Render the same report with both formatters.
- Explain the difference between:
has-a
and:
is-a
Give three examples of each.
- Create an
Orderclass composed ofLineItemobjects.
Each line item should know its own total.
The order should compute the full total by delegating to line items.
- Consider this design:
class UserService(Database):
...Why might this be a bad inheritance relationship?
Rewrite it using composition.
- Draw an object graph for:
App -> UserService -> UserRepository -> Database
App -> EmailService -> EmailSender
Which objects might be shared?
Who might own cleanup?
- Explain why exposing:
car.enginemight be a bad idea.
When might it be acceptable?
- In your own words, explain:
composition connects object boundaries
Use encapsulation, delegation, and ownership in your answer.
In this chapter we learned:
- Composition means building objects from other objects.
- Composition models has-a relationships.
- Inheritance models is-a relationships.
- Objects can collaborate by delegating work to contained objects.
- Encapsulation and composition work together.
- Direct composition means an object creates its own collaborator.
- Dependency injection means collaborators are provided from outside.
- Dependency injection improves flexibility and testing.
- Composition creates object graphs.
- Object graphs affect ownership, lifetime, sharing, and cleanup.
- Shared collaborators should be intentional.
- Composition often avoids unnecessary inheritance.
- Composition can reuse behavior without claiming a false type relationship.
- Fake collaborators make testing easier.
- Composition often depends on behavior rather than exact class.
- Resource-owning collaborators require clear cleanup responsibility.
- Composition should reduce complexity, not create ceremony.
Core model:
composition:
object A has object B
object A uses object B
object A delegates work to object B
Design model:
has-a relationship -> composition
is-a relationship -> inheritance
variable behavior -> composition with swappable collaborator
external effect -> injected collaborator for testability
resource ownership -> explicit cleanup plan
Composition gives object-oriented programs shape.
It lets us build larger systems from focused objects that cooperate through clear interfaces.
Next we study inheritance and method overriding.
Composition showed how objects can use other objects.
Inheritance shows how classes can specialize other classes.
Chapter 47 will study:
- What inheritance means.
- How child classes inherit attributes and methods.
- What method overriding is.
- How inherited behavior is reused.
- How subclass behavior can specialize parent behavior.
- Why inheritance should model is-a relationships.
- How inheritance differs from composition.
- Common inheritance mistakes.
The transition is intentional:
composition handles has-a relationships
inheritance handles is-a relationships
Now that composition is clear, we can study inheritance with better judgment.