Both inheritance and composition reuse code, but they couple classes differently. Inheritance (is-a) binds a subclass to the internals of its base. Composition (has-a) keeps an object inside another and uses only its public interface. The basics are in Composition vs inheritance; this lesson is about the design decision.
When inheritance is justified
- Real is-a. Every subclass fulfils every promise of the base class, so it can be used wherever the base is expected: the Liskov Substitution Principle.
- A stable base designed for extension:
Exception, abstract base classes, framework hooks such asunittest.TestCase. - A shallow tree: one or two levels.
class Bird:
def fly(self):
return "flap"
class Sparrow(Bird):
pass
class Penguin(Bird):
def fly(self):
raise NotImplementedError("penguins cannot fly")
for b in [Sparrow(), Penguin()]:
try:
print(type(b).__name__, b.fly())
except NotImplementedError as e:
print(type(b).__name__, "fails:", e)Sparrow flap
Penguin fails: penguins cannot flyPenguin inherits a fly() it cannot honour. Every caller that loops over Bird objects now needs a special case. When a subclass raises NotImplementedError or silently does nothing in an inherited method, the "is-a" was only an illusion.
The fragile base class
A subclass depends on how the base is implemented, not only on what it promises. Here Bag.add_all calls self.add, and the subclass overrides both methods, so every item is counted twice.