4.5. Inheritance vs. Composition

4.5.1. Rationale

  • Composition over Inheritance

Please excuse me, for code style in this chapter. Instead writing:

>>> class Car:
...     def engine_start(self):
...         pass
...
...     def engine_stop(self):
...         pass

I will write:

>>> class Car:
...     def engine_start(self): pass
...     def engine_stop(self): pass

This way the code is more dense and idea is much clearer to present. There won't be any method implementations in examples.

4.5.2. Problem

  • Code duplication

>>> class Car:
...     def engine_start(self): pass
...     def engine_stop(self): pass
>>>
>>>
>>> class Truck:
...     def engine_start(self): pass
...     def engine_stop(self): pass

4.5.3. Inheritance

>>> class Vehicle:
...     def engine_start(self): pass
...     def engine_stop(self): pass
>>>
>>>
>>> class Car(Vehicle):
...     pass
>>>
>>> class Truck(Vehicle):
...     pass

4.5.4. Inheritance Problem

  • Motorcycle is a vehicle, but doesn't have windows.

>>> class Vehicle:
...     def engine_start(self): pass
...     def engine_stop(self): pass
...     def window_open(self): pass
...     def window_close(self): pass
>>>
>>>
>>> class Car(Vehicle):
...     pass
>>>
>>> class Truck(Vehicle):
...     pass
>>>
>>> class Motorcycle(Vehicle):
...     def window_open(self): raise NotImplementedError
...     def window_close(self): raise NotImplementedError

4.5.5. Multilevel Inheritance

>>> class Vehicle:
...     def engine_start(): pass
...     def engine_stop(): pass
>>>
>>> class VehicleWithWindows(Vehicle):
...     def window_open(): pass
...     def window_close(): pass
>>>
>>>
>>> class Car(VehicleWithWindows):
...     pass
>>>
>>> class Truck(VehicleWithWindows):
...     pass
>>>
>>> class Motorcycle(Vehicle):
...     pass

4.5.6. Composition

>>> class Vehicle:
...     pass
>>>
>>> class Engine:
...     def engine_start(self): pass
...     def engine_stop(self): pass
>>>
>>> class Windows:
...     def window_open(self): pass
...     def window_close(self): pass
>>>
>>>
>>> class Car(Vehicle):
...     engine: Engine
...     window: Windows
>>>
>>> class Truck(Vehicle):
...     engine: Engine
...     window: Windows
>>>
>>> class Motorcycle(Vehicle):
...     engine: Engine

4.5.7. Aggregation

>>> class Vehicle:
...     pass
>>>
>>> class Part:
...     pass
>>>
>>> class Engine(Part):
...     def engine_start(self): pass
...     def engine_stop(self): pass
>>>
>>> class Windows(Part):
...     def window_open(self): pass
...     def window_close(self): pass
>>>
>>>
>>> class Car(Vehicle):
...     parts: list[Part]       # [Engine, Windows]
>>>
>>> class Truck(Vehicle):
...     parts: list[Part]       # [Engine, Windows]
>>>
>>> class Motorcycle(Vehicle):
...     parts: list[Part]       # [Engine]

4.5.8. Mixin Classes

  • More information in Method Resolution Order

>>> class Vehicle:
...     pass
>>>
>>> class HasEngine:
...     def engine_start(self): pass
...     def engine_stop(self): pass
>>>
>>> class HasWindows:
...     def window_open(self): pass
...     def window_close(self): pass
>>>
>>>
>>> class Car(Vehicle, HasEngine, HasWindows):
...     pass
>>>
>>> class Truck(Vehicle, HasEngine, HasWindows):
...     pass
>>>
>>> class Motorcycle(Vehicle, HasEngine):
...     pass

4.5.9. Case Study

Multi level inheritance is a bad pattern here

>>> class ToJSON:
...     def to_json(self):
...         import json
...         return json.dumps(self.__dict__)
>>>
>>> class ToPickle(ToJSON):
...     def to_pickle(self):
...         import pickle
...         return pickle.dumps(self)
>>>
>>>
>>> class Astronaut(ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95I\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut' \
b'\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark' \
b'\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'

Composition:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = {k: v for k, v in vars(self).items() if not k.startswith('_')}
...         return json.dumps(data)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         return pickle.dumps(self)
>>>
>>>
>>> class Astronaut:
...     firstname: str
...     lastname: str
...     __json_serializer: ToJSON
...     __pickle_serializer: ToPickle
...
...     def __init__(self, firstname, lastname, json_serializer=ToJSON, pickle_serializer=ToPickle):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.__json_serializer = json_serializer
...         self.__pickle_serializer = pickle_serializer
...
...     def to_json(self):
...         return self.__json_serializer.to_json(self)
...
...     def to_pickle(self):
...         return self.__pickle_serializer.to_pickle(self)
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95\xa3\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94\x8c\x1b_Astronaut__json_serializer\x94h\x00\x8c\x06ToJSON\x94\x93\x94\x8c\x1d_Astronaut__pickle_serializer\x94h\x00\x8c\x08ToPickle\x94\x93\x94ub.'
>>>
>>>
>>> # It give me ability to write something better
>>> class MyBetterSerializer(ToJSON):
...     def to_json(self):
...         return ...
>>>
>>> astro = Astronaut('Mark', 'Watney', json_serializer=MyBetterSerializer)

Mixin classes - multiple inheritance:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         return json.dumps(self.__dict__)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         return pickle.dumps(self)
>>>
>>>
>>> class Astronaut(ToJSON, ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95I\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut' \
b'\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark' \
b'\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'

4.5.10. Assignments

Code 4.10. Solution
"""
* Assignment: OOP Composition Multilevel
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min

English:
    1. Use data from "Given" section (see below)
    2. Create class `MarsMission` from classes `Habitat`, `Rocket`, `Astronaut`
    3. Use multilevel inheritance
    4. Assignment demonstrates syntax, so do not add any attributes and methods
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `MarsMission` z klas `Habitat`, `Rocket`, `Astronaut`
    3. Użyj wielopoziomowego dziedziczenia
    4. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isclass
    >>> assert isclass(Habitat)
    >>> assert isclass(Astronaut)
    >>> assert isclass(Rocket)
    >>> assert isclass(MarsMission)
    >>> assert issubclass(MarsMission, Habitat)
    >>> assert issubclass(MarsMission, Astronaut)
    >>> assert issubclass(MarsMission, Rocket)
"""


Code 4.11. Solution
"""
* Assignment: OOP Composition Composition
* Complexity: easy
* Lines of code: 2 lines
* Time: 5 min

English:
    1. Use data from "Given" section (see below)
    2. Create class `MarsMission` from classes `Habitat`, `Rocket`, `Astronaut`
    3. Use composition
    4. Assignment demonstrates syntax, so do not add any attributes and methods (only type annotations)
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `MarsMission` z klas `Habitat`, `Rocket`, `Astronaut`
    3. Użyj kompozycji
    4. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod (tylko anotacje typów)
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isclass
    >>> assert isclass(Habitat)
    >>> assert isclass(Astronaut)
    >>> assert isclass(Rocket)
    >>> assert isclass(MarsMission)
    >>> assert issubclass(MarsMission, Habitat)
    >>> assert issubclass(MarsMission, Astronaut)
    >>> assert issubclass(MarsMission, Rocket)
"""


Code 4.12. Solution
"""
* Assignment: OOP Composition Mixin
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min

English:
    1. Use data from "Given" section (see below)
    2. Create class `MarsMission` from classes `Habitat`, `Rocket`, `Astronaut`
    3. Use mixins classes
    4. You can modify given classes
    5. Assignment demonstrates syntax, so do not add any attributes and methods
    6. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Stwórz klasę `MarsMission` z klas `Habitat`, `Rocket`, `Astronaut`
    3. Użyj klas domieszkowych (mixin)
    4. Możesz modyfikować dane klasy
    5. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
    6. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isclass
    >>> assert isclass(Habitat)
    >>> assert isclass(Astronaut)
    >>> assert isclass(Rocket)
    >>> assert isclass(MarsMission)
    >>> assert issubclass(MarsMission, Habitat)
    >>> assert issubclass(MarsMission, Astronaut)
    >>> assert issubclass(MarsMission, Rocket)
"""


Code 4.13. Solution
"""
* Assignment: OOP Composition Decompose
* Complexity: easy
* Lines of code: 30 lines
* Time: 8 min

English:
    1. Use data from "Given" section (see below)
    2. Refactor class `Hero` to use composition
    3. Name mixin classes: `HasHealth` and `HasPosition`
    4. Note, that order of inheritance is important
        a. Try to inherit from `HasPosition`, `HasHealth`
        b. Then `HasHealth`, `HasPosition`
        c. What changes?
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Zrefaktoruj klasę `Hero` aby użyć kompozycji
    3. Nazwij klasy domieszkowe: `HasHealth` i `HasPosition`
    4. Zwróć uwagę, że kolejność dziedziczenia ma znaczenie
        a. Spróbuj dziedziczyć po `HasPosition`, `HasHealth`
        b. A później `HasHealth`, `HasPosition`
        c. Co się zmieniło?
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from random import seed; seed(0)
    >>> from inspect import isclass
    >>> assert isclass(Hero)
    >>> assert isclass(HasHealth)
    >>> assert isclass(HasPosition)
    >>> assert issubclass(Hero, HasHealth)
    >>> assert issubclass(Hero, HasPosition)
    >>> assert hasattr(HasHealth, 'HEALTH_MIN')
    >>> assert hasattr(HasHealth, 'HEALTH_MAX')
    >>> assert hasattr(HasHealth, '_health')
    >>> assert hasattr(HasHealth, 'is_alive')
    >>> assert hasattr(HasHealth, 'is_dead')
    >>> assert hasattr(HasPosition, '_position_x')
    >>> assert hasattr(HasPosition, 'position_set')
    >>> assert hasattr(HasPosition, 'position_change')
    >>> assert hasattr(HasPosition, 'position_get')
    >>> assert hasattr(Hero, 'HEALTH_MIN')
    >>> assert hasattr(Hero, 'HEALTH_MAX')
    >>> assert hasattr(Hero, '_health')
    >>> assert hasattr(Hero, '_position_x')
    >>> assert hasattr(Hero, 'is_alive')
    >>> assert hasattr(Hero, 'is_dead')
    >>> assert hasattr(Hero, 'position_set')
    >>> assert hasattr(Hero, 'position_change')
    >>> assert hasattr(Hero, 'position_get')
    >>> watney = Hero()
    >>> watney.is_alive()
    True
    >>> watney.position_set(x=1, y=2)
    >>> watney.position_change(left=1, up=2)
    >>> watney.position_get()
    (0, 0)
    >>> watney.position_change(right=1, down=2)
    >>> watney.position_get()
    (1, 2)
"""


# Given
from dataclasses import dataclass
from random import randint


@dataclass
class Hero:
    HEALTH_MIN: int = 10
    HEALTH_MAX: int = 20
    _health: int = 0
    _position_x: int = 0
    _position_y: int = 0

    def position_set(self, x: int, y: int) -> None:
        self._position_x = x
        self._position_y = y

    def position_change(self, right=0, left=0, down=0, up=0):
        x = self._position_x + right - left
        y = self._position_y + down - up
        self.position_set(x, y)

    def position_get(self) -> tuple:
        return self._position_x, self._position_y

    def __post_init__(self) -> None:
        self._health = randint(self.HEALTH_MIN, self.HEALTH_MAX)

    def is_alive(self) -> bool:
        return self._health > 0

    def is_dead(self) -> bool:
        return self._health <= 0