Python Composition Practice Problems & Exercises
Practice: Composition vs Inheritance — When to Use Each at Engineering Depth
← Back to lessonEasy
Classify each pair as either an "is-a" (inheritance) or "has-a" (composition) relationship, then verify your answers by running the code.
pairs = [
("Dog", "Animal"),
("Car", "Engine"),
("Manager", "Employee"),
("Rectangle", "Shape"),
("UserProfile", "Database"),
]
RELATIONSHIP = ["is-a", "has-a", "is-a", "is-a", "has-a"]
for (child, parent), rel in zip(pairs, RELATIONSHIP):
print(f"{child} -> {parent}: {rel}")Solution
pairs = [
("Dog", "Animal"),
("Car", "Engine"),
("Manager", "Employee"),
("Rectangle", "Shape"),
("UserProfile", "Database"),
]
RELATIONSHIP = ["is-a", "has-a", "is-a", "is-a", "has-a"]
for (child, parent), rel in zip(pairs, RELATIONSHIP):
print(f"{child} -> {parent}: {rel}")
Explanation: "Is-a" signals inheritance — Dog is a type of Animal. "Has-a" signals composition — a Car has an Engine as a member object. Manager inherits from Employee because a Manager is a specialised Employee. UserProfile holds a reference to a database connection rather than being one.
# Categorise each relationship as "is-a" or "has-a".
# Fill in the RELATIONSHIP list with your answers.
pairs = [
("Dog", "Animal"),
("Car", "Engine"),
("Manager", "Employee"),
("Rectangle", "Shape"),
("UserProfile", "Database"),
]
RELATIONSHIP = [
# TODO: fill each entry with "is-a" or "has-a"
]
for (child, parent), rel in zip(pairs, RELATIONSHIP):
print(f"{child} -> {parent}: {rel}")Expected Output
Dog -> Animal: is-a\nCar -> Engine: has-a\nManager -> Employee: is-a\nRectangle -> Shape: is-a\nUserProfile -> Database: has-aHints
Hint 1: Inheritance expresses "is-a": a Dog IS an Animal.
Hint 2: Composition expresses "has-a": a Car HAS an Engine as a component.
Use composition to give a Car class an Engine. The Car.start() method should delegate to the engine — the simplest form of composition.
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
car = Car()
print(car.start())Solution
class Engine:
def __init__(self):
self._running = False
def start(self):
self._running = True
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine() # has-a Engine
def start(self):
return self.engine.start() # delegation
car = Car()
print(car.start()) # Engine started
Explanation: Car is not an Engine, so inheritance is wrong. Instead we store an Engine as an attribute and forward the start() call. This keeps the two classes independently testable and swappable.
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
# TODO: store an Engine instance as self.engine
pass
def start(self):
# TODO: delegate to self.engine.start()
pass
car = Car()
print(car.start())Expected Output
Engine startedHints
Hint 1: Create an Engine instance inside __init__ and assign it to self.engine.
Hint 2: In Car.start(), call self.engine.start() and return the result.
Complete the LogMixin.log() method so that any class inheriting from it can log messages prefixed with its own class name in square brackets.
class LogMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class Service(LogMixin):
def process(self, data):
self.log(f"Processing: {data}")
return data.upper()
svc = Service()
result = svc.process("hello")
print(result)Solution
class LogMixin:
def log(self, message: str) -> None:
print(f"[{self.__class__.__name__}] {message}")
class Service(LogMixin):
def process(self, data: str) -> str:
self.log(f"Processing: {data}")
return data.upper()
svc = Service()
result = svc.process("hello")
print(result)
# [Service] Processing: hello
# HELLO
Explanation: self.__class__.__name__ resolves to the actual concrete class at runtime, so the same mixin works for any inheriting class. Mixins are narrow, stateless, and reusable — the ideal use case for multiple inheritance in Python.
class LogMixin:
def log(self, message):
# TODO: print "[ClassName] message"
pass
class Service(LogMixin):
def process(self, data):
self.log(f"Processing: {data}")
return data.upper()
svc = Service()
result = svc.process("hello")
print(result)Expected Output
[Service] Processing: hello\nHELLOHints
Hint 1: Use self.__class__.__name__ to get the runtime class name.
Hint 2: Mixins add behaviour through multiple inheritance without storing state.
Refactor DataPipeline from an inheritance-based design to a composition-based one. It should hold a CSVReader as a component and delegate the read operation.
class CSVReader:
def read(self, path):
return f"reading {path}"
class DataPipeline:
def __init__(self):
self.reader = CSVReader()
def run(self, path):
return self.reader.read(path)
pipeline = DataPipeline()
print(pipeline.run("data.csv"))Solution
class CSVReader:
def read(self, path: str) -> str:
return f"reading {path}"
class DataPipeline:
def __init__(self) -> None:
self.reader = CSVReader() # composition: has-a CSVReader
def run(self, path: str) -> str:
return self.reader.read(path) # delegation
pipeline = DataPipeline()
print(pipeline.run("data.csv")) # reading data.csv
Explanation: By holding a CSVReader rather than being one, DataPipeline stays open for extension: you can swap in a JSONReader or ParquetReader without changing DataPipeline's interface.
# Original: inheritance-based design
class CSVReader:
def read(self, path):
return f"reading {path}"
# TODO: rewrite DataPipeline to use composition
# instead of inheriting from CSVReader.
class DataPipeline:
def run(self, path):
# delegate to a CSVReader instance
pass
pipeline = DataPipeline()
print(pipeline.run("data.csv"))Expected Output
reading data.csvHints
Hint 1: Store a CSVReader() in __init__ as self.reader.
Hint 2: Call self.reader.read(path) from run().
Medium
Implement OrderService so it accepts any object that satisfies the Notifier protocol. The service should work with both EmailNotifier and SMSNotifier without modification.
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None: ...
class EmailNotifier:
def send(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier:
def send(self, message: str) -> None:
print(f"SMS: {message}")
class OrderService:
def __init__(self, notifier: Notifier) -> None:
self.notifier = notifier
def place_order(self, item: str) -> None:
msg = f"Order placed: {item}"
print(msg)
self.notifier.send(msg)
svc = OrderService(EmailNotifier())
svc.place_order("Book")Solution
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None: ...
class EmailNotifier:
def send(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier:
def send(self, message: str) -> None:
print(f"SMS: {message}")
class OrderService:
def __init__(self, notifier: Notifier) -> None:
self.notifier = notifier # dependency injected via constructor
def place_order(self, item: str) -> None:
msg = f"Order placed: {item}"
print(msg)
self.notifier.send(msg)
# Swap notifier without touching OrderService:
svc = OrderService(EmailNotifier())
svc.place_order("Book")
svc2 = OrderService(SMSNotifier())
svc2.place_order("Pen")
Explanation: typing.Protocol provides structural typing — any class with a matching send() method satisfies Notifier without explicit inheritance. Constructor injection makes OrderService testable: in unit tests you pass a fake notifier that captures messages rather than sending real emails.
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None:
...
class EmailNotifier:
def send(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier:
def send(self, message: str) -> None:
print(f"SMS: {message}")
class OrderService:
def __init__(self, notifier):
# TODO: store notifier
pass
def place_order(self, item: str) -> None:
# TODO: print "Order placed: {item}" then notify
pass
svc = OrderService(EmailNotifier())
svc.place_order("Book")Expected Output
Order placed: Book\nEmail: Order placed: BookHints
Hint 1: Store the injected notifier as self.notifier.
Hint 2: Call self.notifier.send(...) after printing the order confirmation.
Implement two mixins: ValidateMixin raises ValueError if any attribute is None, and SerializeMixin serialises the object to JSON. Combine them in a User class.
import json
class ValidateMixin:
def validate(self):
for key, val in self.__dict__.items():
if val is None:
raise ValueError(f"{key} must not be None")
class SerializeMixin:
def to_json(self):
return json.dumps(self.__dict__)
class User(ValidateMixin, SerializeMixin):
def __init__(self, name, email):
self.name = name
self.email = email
u = User("Alice", "[email protected]")
u.validate()
print(u.to_json())Solution
import json
class ValidateMixin:
def validate(self) -> None:
for key, val in self.__dict__.items():
if val is None:
raise ValueError(f"Field '{key}' must not be None")
class SerializeMixin:
def to_json(self) -> str:
return json.dumps(self.__dict__)
class User(ValidateMixin, SerializeMixin):
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
u.validate() # no error
# Fails validation:
try:
bad = User("Bob", None)
bad.validate()
except ValueError as e:
print(e) # Field 'email' must not be None
Explanation: Mixins add orthogonal capabilities. ValidateMixin knows nothing about serialisation and vice versa — they compose cleanly. Python's MRO ensures validate() and to_json() resolve without conflict because neither mixin defines the same method.
import json
class ValidateMixin:
def validate(self):
# TODO: raise ValueError if any attribute value is None
for key, val in self.__dict__.items():
pass
class SerializeMixin:
def to_json(self):
# TODO: return json.dumps of __dict__
pass
class User(ValidateMixin, SerializeMixin):
def __init__(self, name, email):
self.name = name
self.email = email
u = User("Alice", "[email protected]")
u.validate()
print(u.to_json())Expected Output
{"name": "Alice", "email": "[email protected]"}Hints
Hint 1: In ValidateMixin.validate(), iterate self.__dict__.items() and raise ValueError if any value is None.
Hint 2: In SerializeMixin.to_json(), return json.dumps(self.__dict__).
Implement the Strategy pattern using composition and typing.Protocol. The Sorter class should accept any strategy that satisfies SortStrategy and delegate to it.
from typing import Protocol
class SortStrategy(Protocol):
def sort(self, data: list) -> list: ...
class BubbleSort:
def sort(self, data: list) -> list:
data = list(data)
n = len(data)
for i in range(n):
for j in range(n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
class PythonSort:
def sort(self, data: list) -> list:
return sorted(data)
class Sorter:
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def execute(self, data: list) -> list:
return self._strategy.sort(data)
s = Sorter(PythonSort())
print(s.execute([3, 1, 4, 1, 5]))Solution
from typing import Protocol
class SortStrategy(Protocol):
def sort(self, data: list) -> list: ...
class BubbleSort:
def sort(self, data: list) -> list:
arr = list(data)
n = len(arr)
for i in range(n):
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
class PythonSort:
def sort(self, data: list) -> list:
return sorted(data)
class Sorter:
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def swap_strategy(self, strategy: SortStrategy) -> None:
self._strategy = strategy # hot-swap at runtime
def execute(self, data: list) -> list:
return self._strategy.sort(data)
s = Sorter(BubbleSort())
print(s.execute([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
s.swap_strategy(PythonSort())
print(s.execute([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
Explanation: The Strategy pattern is composition at its most explicit. Sorter holds a strategy object and delegates work to it. Swapping strategies at runtime is trivial and requires zero changes to Sorter. The Protocol ensures structural type safety without forcing strategies to inherit from a base class.
from typing import Protocol
class SortStrategy(Protocol):
def sort(self, data: list) -> list: ...
class BubbleSort:
def sort(self, data: list) -> list:
# TODO: implement bubble sort
pass
class PythonSort:
def sort(self, data: list) -> list:
# TODO: use sorted()
pass
class Sorter:
def __init__(self, strategy):
self._strategy = strategy
def execute(self, data: list) -> list:
return self._strategy.sort(data)
s = Sorter(PythonSort())
print(s.execute([3, 1, 4, 1, 5]))Expected Output
[1, 1, 3, 4, 5]Hints
Hint 1: BubbleSort needs a nested loop comparing adjacent elements and swapping.
Hint 2: PythonSort can return sorted(data).
Implement __getattr__ on TimestampedLogger so that any method not explicitly overridden (like flush) is transparently forwarded to the wrapped FileLogger.
class FileLogger:
def write(self, msg: str) -> None:
print(f"FILE: {msg}")
def flush(self) -> None:
print("FILE: flushed")
class TimestampedLogger:
def __init__(self, logger):
self._logger = logger
self._prefix = "2026-03-21"
def write(self, msg: str) -> None:
self._logger.write(f"{self._prefix} {msg}")
def __getattr__(self, name: str):
return getattr(self._logger, name)
tl = TimestampedLogger(FileLogger())
tl.write("Starting up")
tl.flush()Solution
class FileLogger:
def write(self, msg: str) -> None:
print(f"FILE: {msg}")
def flush(self) -> None:
print("FILE: flushed")
def close(self) -> None:
print("FILE: closed")
class TimestampedLogger:
def __init__(self, logger: FileLogger) -> None:
self._logger = logger
self._prefix = "2026-03-21"
def write(self, msg: str) -> None:
# Override to prepend timestamp
self._logger.write(f"{self._prefix} {msg}")
def __getattr__(self, name: str):
# Transparent delegation: any attr not found on self
# is looked up on the wrapped logger.
return getattr(self._logger, name)
tl = TimestampedLogger(FileLogger())
tl.write("Starting up") # FILE: 2026-03-21 Starting up
tl.flush() # FILE: flushed (delegated)
tl.close() # FILE: closed (also delegated)
Explanation: __getattr__ is only triggered when Python cannot find the attribute through normal means. This makes it ideal for transparent delegation wrappers — you explicitly override what you need to intercept, and let everything else fall through to the wrapped object via getattr.
class FileLogger:
def write(self, msg: str) -> None:
print(f"FILE: {msg}")
def flush(self) -> None:
print("FILE: flushed")
class TimestampedLogger:
def __init__(self, logger):
self._logger = logger
self._prefix = "2026-03-21"
def write(self, msg: str) -> None:
self._logger.write(f"{self._prefix} {msg}")
def __getattr__(self, name):
# TODO: forward any unknown attribute to self._logger
pass
tl = TimestampedLogger(FileLogger())
tl.write("Starting up")
tl.flush()Expected Output
FILE: 2026-03-21 Starting up\nFILE: flushedHints
Hint 1: __getattr__ is only called when normal attribute lookup fails.
Hint 2: Return getattr(self._logger, name) to delegate transparently.
Hard
Build a composable Pipeline that chains Step objects. Each step transforms a string. The pipeline passes the output of one step as the input to the next, enabling any combination of steps without changing the pipeline class.
from typing import Protocol, List
class Step(Protocol):
def process(self, data: str) -> str: ...
class StripStep:
def process(self, data: str) -> str:
return data.strip()
class UpperStep:
def process(self, data: str) -> str:
return data.upper()
class ReplaceStep:
def __init__(self, old: str, new: str) -> None:
self.old = old
self.new = new
def process(self, data: str) -> str:
return data.replace(self.old, self.new)
class Pipeline:
def __init__(self, steps: List[Step]) -> None:
self.steps = steps
def run(self, data: str) -> str:
for step in self.steps:
data = step.process(data)
return data
p = Pipeline([StripStep(), UpperStep()])
print(repr(p.run(" hello world ")))Solution
from typing import Protocol, List
class Step(Protocol):
def process(self, data: str) -> str: ...
class StripStep:
def process(self, data: str) -> str:
return data.strip()
class UpperStep:
def process(self, data: str) -> str:
return data.upper()
class ReplaceStep:
def __init__(self, old: str, new: str) -> None:
self.old, self.new = old, new
def process(self, data: str) -> str:
return data.replace(self.old, self.new)
class Pipeline:
def __init__(self, steps: List[Step]) -> None:
self.steps = steps
def add_step(self, step: Step) -> "Pipeline":
self.steps.append(step)
return self # fluent interface
def run(self, data: str) -> str:
for step in self.steps:
data = step.process(data)
return data
p = Pipeline([StripStep(), ReplaceStep("world", "Python"), UpperStep()])
print(repr(p.run(" hello world "))) # 'HELLO PYTHON'
Explanation: The pipeline itself knows nothing about individual transformations — it simply owns a list of Step objects and chains their outputs. Adding a new transformation requires zero changes to Pipeline. This is the Open/Closed Principle enacted through composition. The Protocol keeps the design structurally typed without demanding inheritance.
from typing import Protocol, List
class Step(Protocol):
def process(self, data: str) -> str: ...
class StripStep:
def process(self, data: str) -> str:
return data.strip()
class UpperStep:
def process(self, data: str) -> str:
return data.upper()
class Pipeline:
def __init__(self, steps):
# TODO: store steps
pass
def run(self, data: str) -> str:
# TODO: pass data through each step in order
pass
p = Pipeline([StripStep(), UpperStep()])
print(repr(p.run(" hello world ")))Expected Output
'HELLO WORLD'Hints
Hint 1: Store the steps list in __init__.
Hint 2: In run(), iterate over self.steps and pass the result of each step into the next.
Create CachedAuditedStore by combining AuditMixin, CacheMixin, and DataStore. All three use cooperative super().__init__(**kwargs) — your task is to compose them correctly and demonstrate the combined behaviour.
class AuditMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._audit_log = []
def record(self, action: str) -> None:
self._audit_log.append(action)
def audit_trail(self) -> list:
return list(self._audit_log)
class CacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cache = {}
def cache_get(self, key: str):
return self._cache.get(key)
def cache_set(self, key: str, value) -> None:
self._cache[key] = value
class DataStore:
def __init__(self, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
def fetch(self, key: str) -> str:
return f"{self.name}:{key}"
class CachedAuditedStore(AuditMixin, CacheMixin, DataStore):
pass
store = CachedAuditedStore(name="users")
val = store.fetch("user_1")
store.cache_set("user_1", val)
store.record(f"fetched {val}")
print(store.cache_get("user_1"))
print(store.audit_trail())Solution
class AuditMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._audit_log: list = []
def record(self, action: str) -> None:
self._audit_log.append(action)
def audit_trail(self) -> list:
return list(self._audit_log)
class CacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cache: dict = {}
def cache_get(self, key: str):
return self._cache.get(key)
def cache_set(self, key: str, value) -> None:
self._cache[key] = value
class DataStore:
def __init__(self, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
def fetch(self, key: str) -> str:
return f"{self.name}:{key}"
# MRO: CachedAuditedStore -> AuditMixin -> CacheMixin -> DataStore -> object
class CachedAuditedStore(AuditMixin, CacheMixin, DataStore):
pass
store = CachedAuditedStore(name="users")
val = store.fetch("user_1")
store.cache_set("user_1", val)
store.record(f"fetched {val}")
print(store.cache_get("user_1")) # users:user_1
print(store.audit_trail()) # ['fetched users:user_1']
# Inspect MRO:
print([c.__name__ for c in CachedAuditedStore.__mro__])
Explanation: The cooperative super().__init__(**kwargs) chain ensures every __init__ in the MRO is called exactly once and in the correct order. AuditMixin and CacheMixin each initialise their own private state without knowing about each other. name travels through the **kwargs pipeline until it reaches DataStore. This is the canonical pattern for mixin composition in Python.
class AuditMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._audit_log = []
def record(self, action: str) -> None:
self._audit_log.append(action)
def audit_trail(self) -> list:
return list(self._audit_log)
class CacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cache = {}
def cache_get(self, key: str):
return self._cache.get(key)
def cache_set(self, key: str, value) -> None:
self._cache[key] = value
class DataStore:
def __init__(self, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
def fetch(self, key: str) -> str:
return f"{self.name}:{key}"
# TODO: create CachedAuditedStore combining all three
# Then demonstrate: fetch a key, cache it, record the action.
store = CachedAuditedStore(name="users")
val = store.fetch("user_1")
store.cache_set("user_1", val)
store.record(f"fetched {val}")
print(store.cache_get("user_1"))
print(store.audit_trail())Expected Output
users:user_1\n['fetched users:user_1']Hints
Hint 1: Combine AuditMixin, CacheMixin, DataStore in that order.
Hint 2: All three __init__ methods use **kwargs and call super().__init__(**kwargs) so the MRO chain works correctly.
Hint 3: CachedAuditedStore just needs to pass name through to DataStore.
Refactor the four-level inheritance chain into a flat TrainedDog class that uses composition only. Create separate behaviour classes and inject them. The public API (breathe, cuddle, bark, sit) must remain unchanged.
class BreathBehaviour:
def breathe(self) -> str:
return "breathing"
class PetBehaviour:
def cuddle(self) -> str:
return "cuddling"
class DogBehaviour:
def bark(self) -> str:
return "woof"
class TrainingBehaviour:
def sit(self) -> str:
return "sitting"
class TrainedDog:
def __init__(self):
self._breath = BreathBehaviour()
self._pet = PetBehaviour()
self._dog = DogBehaviour()
self._training = TrainingBehaviour()
def breathe(self) -> str:
return self._breath.breathe()
def cuddle(self) -> str:
return self._pet.cuddle()
def bark(self) -> str:
return self._dog.bark()
def sit(self) -> str:
return self._training.sit()
td = TrainedDog()
print(td.breathe())
print(td.cuddle())
print(td.bark())
print(td.sit())Solution
from typing import Protocol
class Breather(Protocol):
def breathe(self) -> str: ...
class Cuddler(Protocol):
def cuddle(self) -> str: ...
class Barker(Protocol):
def bark(self) -> str: ...
class Trainer(Protocol):
def sit(self) -> str: ...
class BreathBehaviour:
def breathe(self) -> str:
return "breathing"
class PetBehaviour:
def cuddle(self) -> str:
return "cuddling"
class DogBehaviour:
def bark(self) -> str:
return "woof"
class TrainingBehaviour:
def sit(self) -> str:
return "sitting"
class TrainedDog:
def __init__(
self,
breath: Breather = None,
pet: Cuddler = None,
dog: Barker = None,
training: Trainer = None,
) -> None:
self._breath = breath or BreathBehaviour()
self._pet = pet or PetBehaviour()
self._dog = dog or DogBehaviour()
self._training = training or TrainingBehaviour()
def breathe(self) -> str:
return self._breath.breathe()
def cuddle(self) -> str:
return self._pet.cuddle()
def bark(self) -> str:
return self._dog.bark()
def sit(self) -> str:
return self._training.sit()
td = TrainedDog()
print(td.breathe()) # breathing
print(td.cuddle()) # cuddling
print(td.bark()) # woof
print(td.sit()) # sitting
Explanation: The deep hierarchy created a fragile coupling chain — changing Animal affected every subclass four levels below. The composed version is flat: each behaviour is an independent object that can be tested, swapped, and evolved without touching any other. The optional constructor injection also enables unit testing with fakes, and means future variants (e.g., a RobotDog with no PetBehaviour) can substitute any component independently.
# Fragile deep hierarchy — refactor this
class Animal:
def breathe(self): return "breathing"
class Pet(Animal):
def cuddle(self): return "cuddling"
class Dog(Pet):
def bark(self): return "woof"
class TrainedDog(Dog):
def sit(self): return "sitting"
# TODO: redesign using composition + Protocol so that
# TrainedDog does not inherit from any of the above.
# It should hold behaviour objects and delegate.
class TrainedDog:
pass
td = TrainedDog()
print(td.breathe())
print(td.cuddle())
print(td.bark())
print(td.sit())Expected Output
breathing\ncuddling\nwoof\nsittingHints
Hint 1: Create separate behaviour classes: BreathBehaviour, PetBehaviour, DogBehaviour, TrainingBehaviour.
Hint 2: Store them in TrainedDog.__init__ and delegate each method to the matching behaviour object.
