4.5. Inheritance vs. Composition

4.5.1. Rationale

  • Composition over Inheritance

4.5.2. 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

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 Motorbike(Vehicle):
    """Motorbike is a vehicle, but doesn't have windows."""

    def window_open(self):
        raise NotImplementedError

    def window_close(self):
        raise NotImplementedError

4.5.5. Composition

  • Mixin Classes

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 Motorbike(Vehicle, HasEngine):
    pass

4.5.6. Case Study

Multi level inheritance is a bad pattern here:

class A:
    pass

class B(A):
    pass

class C(B):
    pass
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.'

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.7. Assignments

Code 4.72. Solution
"""
* Assignment: OOP Composition Syntax
* Filename: oop_composition_syntax.py
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min

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

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Skomponuj klasę `MarsMission` z `Habitat`, `Rocket`, `Astronaut`
    3. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
    4. 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)
"""


# Given
class Habitat:
    pass


class Astronaut:
    pass


class Rocket:
    pass


Code 4.73. Solution
"""
* Assignment: OOP Composition Decompose
* Filename: oop_composition_decompose.py
* Complexity: easy
* Lines of code: 30 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. Refactor class `Hero` to use composition
    3. Name mixin classes: `HasHealth` and `HasPosition`
    3. 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`
    3. 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 __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

    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[int, int]:
        return self._position_x, self._position_y