Composition builds an object from parts
Composition means building an object out of other objects, so it has parts instead of being a kind of something. Many designers prefer composition over inheritance, because parts can be swapped while the program runs. A report that holds a style object can change its look without a new class.
By the end of this lesson
Note
I can choose composition over inheritance and say why.
Key words
| Word | What it means |
|---|---|
| Composition | building an object from other objects |
| Is-a | the link between a subclass and its parent |
| Has-a | the link between an object and its parts |
| Delegation | passing a job on to a part |
| Component | an object used as a part of another |
The cells students fill in are left blank in this preview.
Is-a and has-a are different links
A DiskCheck is a Check, so inheritance fits. A car has an engine. It is not a kind of engine. When one object has another as a part, you do not inherit. You store the part in an attribute and call it.
This is composition. When the object passes a job on to the part, that is delegation.
Two interchangeable style parts
class Plain:
def line(self, label, value):
return label + ": " + str(value)
class Table:
def line(self, label, value):
return label + " | " + str(value)A Report that has a style
class Report:
def __init__(self, style):
self.style = style
self.rows = []
def add(self, label, value):
self.rows.append([label, value])
def set_style(self, style):
self.style = style
def render(self):
lines = []
for row in self.rows:
lines.append(self.style.line(row[0], row[1]))
return linesMake a folder for this lesson
mkdir -p ~/oop3Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Go into it
cd ~/oop3Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the classes to report.py
printf 'class Plain:\n def line(self, label, value):\n return label + ": " + str(value)\n\nclass Table:\n def line(self, label, value):\n return label + " | " + str(value)\n\nclass Report:\n def __init__(self, style):\n self.style = style\n self.rows = []\n\n def add(self, label, value):\n self.rows.append([label, value])\n\n def set_style(self, style):\n self.style = style\n\n def render(self):\n lines = []\n for row in self.rows:\n lines.append(self.style.line(row[0], row[1]))\n return lines\n' > report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A part can be swapped while running
The report does not format anything. It asks self.style.line(...) to do it. Any object with a line method will work.
set_style swaps the part. The same report, holding the same rows, now looks different. With inheritance the look would be fixed when the object was made.
Swap the style and show the report again
python3 -c "from report import Report, Plain, Table; r = Report(Plain()); r.add('cpu', 40); r.add('disk', 71); print(r.render()); r.set_style(Table()); print(r.render())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Inheritance grows a class for every mix
Suppose reports can use two styles and send to two places. With inheritance you write a class for every mix: plain to screen, plain to file, table to screen, table to file.
With composition you write two style classes and two place classes, and a report holds one of each. Every extra option adds one class, not a whole new row.
Worked example: classes needed
| Styles | Places | Inheritance | Composition |
|---|---|---|---|
| 2 | 2 | 4 classes | 4 classes |
| 3 | 3 | 9 classes | 6 classes |
| 4 | 5 | 20 classes | 9 classes |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think composition just means more code, so inheritance is always shorter. For two classes it can be. But inheritance fixes the behaviour when the object is made and grows with every mix. Use is-a only when it is plainly true, and has-a otherwise.