6.1 Building classes and composition¶
Learning objectives¶
By the end of this section, you will be able to:
-
Define classes with proper constructors and methods in Python
-
Understand the difference between class definition and object instantiation
-
Use composition to build complex objects from simpler components
-
Apply the principle of “composition over inheritance” for code reuse
-
Design classes that are cohesive, focused, and easy to test
-
Create well-structured class hierarchies using composition relationships
Defining classes in Python¶
A class is a blueprint that defines the structure and behavior of objects. It specifies what attributes (data) and methods (functions) objects of that type will have.
Basic class syntax¶
class Student:
"""A class to represent a student"""
def __init__(self, name, student_id):
"""Constructor - called when creating a new Student object"""
self.name = name
self.student_id = student_id
self.grades = [] # Initialize empty list of grades
def add_grade(self, grade):
"""Method to add a grade to the student's record"""
if 0 <= grade <= 100:
self.grades.append(grade)
return True
return False
def get_average(self):
"""Method to calculate the student's average grade"""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def __str__(self):
"""String representation of the student"""
return f"Student({self.name}, ID: {self.student_id})"
# Creating objects (instantiation)
alice = Student("Alice Johnson", "S12345")
bob = Student("Bob Smith", "S12346")
# Using the objects
alice.add_grade(85)
alice.add_grade(92)
print(f"{alice.name}'s average: {alice.get_average()}") # Alice's average: 88.5
Key components explained¶
1. Class definition: class Student: creates a new class named Student
2. Constructor: __init__() is a special method called when creating new objects
-
Takes
selfas the first parameter (refers to the object being created) -
Initializes the object’s attributes
-
Sets up the initial state
3. Instance attributes: self.name, self.student_id, self.grades
-
Data that belongs to each individual object
-
Different objects can have different values
4. Methods: Functions that belong to the class
-
Always take
selfas the first parameter -
Can access and modify the object’s attributes
-
Define what the object can do
5. Special methods: __str__() defines how objects are displayed as strings
Understanding constructors¶
The constructor (__init__) is responsible for setting up a new object’s initial state. It’s one of the most important methods in a class.
Constructor best practices¶
class BankAccount:
"""A simple bank account class demonstrating good constructor practices"""
def __init__(self, account_number, owner_name, initial_balance=0):
"""
Initialize a new bank account
Args:
account_number (str): Unique account identifier
owner_name (str): Name of the account owner
initial_balance (float): Starting balance (default: 0)
"""
# Validate inputs
if not account_number or not account_number.strip():
raise ValueError("Account number cannot be empty")
if not owner_name or not owner_name.strip():
raise ValueError("Owner name cannot be empty")
if initial_balance < 0:
raise ValueError("Initial balance cannot be negative")
# Set up instance attributes
self.account_number = account_number.strip()
self.owner_name = owner_name.strip()
self.balance = initial_balance
self.transaction_history = [] # Start with empty history
# Record the initial deposit if any
if initial_balance > 0:
self.transaction_history.append(f"Initial deposit: +${initial_balance}")
# Good usage
account = BankAccount("ACC001", "Alice Johnson", 1000)
# Constructor validation prevents bad data
try:
bad_account = BankAccount("", "Bob", -100) # Will raise ValueError
except ValueError as e:
print(f"Error: {e}")
Constructor parameters and defaults¶
class Rectangle:
"""Rectangle class showing different parameter patterns"""
def __init__(self, width, height=None):
"""
Create a rectangle
Args:
width (float): Width of rectangle
height (float, optional): Height of rectangle. If None, creates a square
"""
if width <= 0:
raise ValueError("Width must be positive")
self.width = width
# If height not provided, make it a square
if height is None:
self.height = width
else:
if height <= 0:
raise ValueError("Height must be positive")
self.height = height
def area(self):
return self.width * self.height
def is_square(self):
return self.width == self.height
# Different ways to create rectangles
rect1 = Rectangle(5, 3) # Rectangle: 5x3
rect2 = Rectangle(4) # Square: 4x4 (height defaults to width)
print(f"Rectangle area: {rect1.area()}") # 15
print(f"Square area: {rect2.area()}") # 16
print(f"Is rect2 a square? {rect2.is_square()}") # True
Methods: defining behavior¶
Methods are functions that belong to a class and define what objects of that class can do. They have access to the object’s data and can modify it.
Instance methods¶
class Counter:
"""A simple counter class demonstrating various method patterns"""
def __init__(self, start_value=0):
"""Initialize counter with starting value"""
self.value = start_value
self.history = [start_value] # Track all values
def increment(self, amount=1):
"""Increase counter by specified amount (default: 1)"""
if amount <= 0:
raise ValueError("Increment amount must be positive")
self.value += amount
self.history.append(self.value)
def decrement(self, amount=1):
"""Decrease counter by specified amount (default: 1)"""
if amount <= 0:
raise ValueError("Decrement amount must be positive")
self.value -= amount
self.history.append(self.value)
def reset(self, new_value=0):
"""Reset counter to specified value (default: 0)"""
self.value = new_value
self.history.append(self.value)
def get_value(self):
"""Get current counter value"""
return self.value
def get_history(self):
"""Get list of all values the counter has had"""
return self.history.copy() # Return copy to protect internal data
def __str__(self):
return f"Counter(value={self.value})"
# Using the counter
counter = Counter(10)
counter.increment(5) # value = 15
counter.decrement(3) # value = 12
counter.increment() # value = 13 (default increment of 1)
print(counter) # Counter(value=13)
print(counter.get_history()) # [10, 15, 12, 13]
Method design principles¶
1. Single responsibility: Each method should do one thing well
class EmailValidator:
"""Class demonstrating single responsibility in methods"""
def is_valid_email(self, email):
"""Check if email format is valid"""
return self._has_at_symbol(email) and self._has_domain(email)
def _has_at_symbol(self, email):
"""Private helper: check for @ symbol"""
return '@' in email and email.count('@') == 1
def _has_domain(self, email):
"""Private helper: check for domain part"""
if '@' not in email:
return False
domain = email.split('@')[1]
return '.' in domain and len(domain) > 3
2. Meaningful names: Method names should clearly describe what they do
# Good method names
user.authenticate(password)
account.withdraw(amount)
list.append(item)
validator.is_valid_email(email)
# Poor method names
user.check(password) # Check what?
account.do_money(amount) # Do what with money?
list.add_thing(item) # Vague
validator.test(email) # Test what?
3. Consistent parameter patterns: Similar methods should have similar signatures
class MathOperations:
"""Consistent parameter patterns"""
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Composition: building with components¶
Composition is a design principle where complex objects are built by combining simpler objects, rather than inheriting from parent classes. It represents a “has-a” relationship.
Why composition over inheritance?¶
Inheritance problems:
-
Rigid hierarchy that’s hard to change
-
Child classes tightly coupled to parent classes
-
Can lead to complex inheritance chains
-
“Diamond problem” with multiple inheritance
Composition benefits:
-
Flexible - can change components at runtime
-
Loosely coupled - components can be developed independently
-
Easier to test - can test components separately
-
More maintainable - changes to one component don’t affect others
Basic composition example
class Engine:
"""Simple engine component"""
def __init__(self, horsepower, fuel_type):
self.horsepower = horsepower
self.fuel_type = fuel_type
self.is_running = False
def start(self):
if not self.is_running:
self.is_running = True
return f"{self.horsepower}hp {self.fuel_type} engine started"
return "Engine already running"
def stop(self):
if self.is_running:
self.is_running = False
return "Engine stopped"
return "Engine already stopped"
class Wheels:
"""Wheel system component"""
def __init__(self, wheel_count, wheel_size):
self.wheel_count = wheel_count
self.wheel_size = wheel_size
def get_info(self):
return f"{self.wheel_count} wheels, {self.wheel_size} inch diameter"
class Car:
"""Car class using composition instead of inheritance"""
def __init__(self, make, model, engine, wheels):
self.make = make
self.model = model
self.engine = engine # Composition: Car HAS-A Engine
self.wheels = wheels # Composition: Car HAS-A Wheels system
self.speed = 0
def start(self):
"""Start the car by starting its engine"""
result = self.engine.start()
return f"{self.make} {self.model}: {result}"
def stop(self):
"""Stop the car"""
self.speed = 0
result = self.engine.stop()
return f"{self.make} {self.model}: {result}"
def accelerate(self, amount):
"""Accelerate if engine is running"""
if self.engine.is_running:
self.speed += amount
return f"Accelerating to {self.speed} km/h"
return "Cannot accelerate - engine not running"
def get_specs(self):
"""Get car specifications"""
return f"{self.make} {self.model} - {self.engine.horsepower}hp {self.engine.fuel_type}, {self.wheels.get_info()}"
# Creating a car using composition
v8_engine = Engine(400, "gasoline")
standard_wheels = Wheels(4, 18)
sports_car = Car("Ferrari", "F40", v8_engine, standard_wheels)
print(sports_car.start()) # Ferrari F40: 400hp gasoline engine started
print(sports_car.accelerate(50)) # Accelerating to 50 km/h
print(sports_car.get_specs()) # Ferrari F40 - 400hp gasoline, 4 wheels, 18 inch diameter
Advanced composition patterns¶
1. Composition with dependency injection:
class FileLogger:
"""Logs messages to a file"""
def __init__(self, filename):
self.filename = filename
def log(self, message):
with open(self.filename, 'a') as f:
f.write(f"{message}\n")
class ConsoleLogger:
"""Logs messages to console"""
def log(self, message):
print(f"LOG: {message}")
class ShoppingCart:
"""Shopping cart that can use different logging strategies"""
def __init__(self, logger):
self.items = []
self.logger = logger # Composition: inject any logger
def add_item(self, item, price):
self.items.append({"item": item, "price": price})
self.logger.log(f"Added {item} (${price}) to cart")
def remove_item(self, item):
self.items = [i for i in self.items if i["item"] != item]
self.logger.log(f"Removed {item} from cart")
def get_total(self):
total = sum(item["price"] for item in self.items)
self.logger.log(f"Calculated total: ${total}")
return total
# Using different loggers with the same cart
console_logger = ConsoleLogger()
file_logger = FileLogger("cart.log")
cart1 = ShoppingCart(console_logger) # Logs to console
cart2 = ShoppingCart(file_logger) # Logs to file
cart1.add_item("Book", 25.99) # Prints: LOG: Added Book ($25.99) to cart
cart2.add_item("Phone", 699.99) # Writes to cart.log file
2. Complex composition with multiple components:
class Display:
"""Display component for devices"""
def __init__(self, width, height, resolution):
self.width = width
self.height = height
self.resolution = resolution
self.is_on = False
def turn_on(self):
self.is_on = True
return f"Display on: {self.width}x{self.height} at {self.resolution}"
def turn_off(self):
self.is_on = False
return "Display off"
class Battery:
"""Battery component for portable devices"""
def __init__(self, capacity_mah):
self.capacity_mah = capacity_mah
self.current_charge = capacity_mah # Start fully charged
def get_charge_percentage(self):
return (self.current_charge / self.capacity_mah) * 100
def use_power(self, amount_mah):
"""Use some battery power"""
self.current_charge = max(0, self.current_charge - amount_mah)
def charge(self, amount_mah):
"""Charge the battery"""
self.current_charge = min(self.capacity_mah, self.current_charge + amount_mah)
class CPU:
"""CPU component"""
def __init__(self, speed_ghz, cores):
self.speed_ghz = speed_ghz
self.cores = cores
self.is_active = False
def start(self):
self.is_active = True
return f"CPU started: {self.cores} cores at {self.speed_ghz}GHz"
def stop(self):
self.is_active = False
return "CPU stopped"
class Laptop:
"""Laptop composed of multiple components"""
def __init__(self, brand, model, cpu, display, battery):
self.brand = brand
self.model = model
self.cpu = cpu # Composition
self.display = display # Composition
self.battery = battery # Composition
self.is_running = False
def power_on(self):
"""Power on the laptop"""
if self.battery.get_charge_percentage() < 5:
return "Cannot power on: battery too low"
self.is_running = True
cpu_msg = self.cpu.start()
display_msg = self.display.turn_on()
# Use some power to start up
self.battery.use_power(50)
return f"{self.brand} {self.model} powered on\n{cpu_msg}\n{display_msg}"
def power_off(self):
"""Power off the laptop"""
self.is_running = False
cpu_msg = self.cpu.stop()
display_msg = self.display.turn_off()
return f"{self.brand} {self.model} powered off\n{cpu_msg}\n{display_msg}"
def get_status(self):
"""Get current laptop status"""
battery_pct = self.battery.get_charge_percentage()
status = "ON" if self.is_running else "OFF"
return f"{self.brand} {self.model}: {status}, Battery: {battery_pct:.1f}%"
# Creating a laptop with composed components
i7_cpu = CPU(2.8, 4)
hd_display = Display(1920, 1080, "1080p")
lithium_battery = Battery(5000) # 5000 mAh
macbook = Laptop("Apple", "MacBook Pro", i7_cpu, hd_display, lithium_battery)
print(macbook.power_on())
print(macbook.get_status())
print(macbook.power_off())
Building cohesive classes¶
Cohesion means that a class has a single, well-defined purpose and all its methods work together to support that purpose.
High cohesion example
class PasswordValidator:
"""Highly cohesive class - everything relates to password validation"""
def __init__(self, min_length=8, require_uppercase=True, require_numbers=True):
self.min_length = min_length
self.require_uppercase = require_uppercase
self.require_numbers = require_numbers
def is_valid(self, password):
"""Main validation method that uses all helper methods"""
return (self._check_length(password) and
self._check_uppercase(password) and
self._check_numbers(password))
def _check_length(self, password):
"""Check if password meets minimum length"""
return len(password) >= self.min_length
def _check_uppercase(self, password):
"""Check if password has uppercase letters (if required)"""
if not self.require_uppercase:
return True
return any(c.isupper() for c in password)
def _check_numbers(self, password):
"""Check if password has numbers (if required)"""
if not self.require_numbers:
return True
return any(c.isdigit() for c in password)
def get_requirements(self):
"""Return a description of password requirements"""
requirements = [f"At least {self.min_length} characters"]
if self.require_uppercase:
requirements.append("At least one uppercase letter")
if self.require_numbers:
requirements.append("At least one number")
return requirements
# Usage
validator = PasswordValidator(min_length=10, require_uppercase=True, require_numbers=True)
print(validator.is_valid("MyPassword123")) # True
print(validator.get_requirements()) # List of requirements
Low cohesion example (what to avoid)
class UserAccount:
"""Low cohesion - too many unrelated responsibilities"""
def __init__(self, username, email):
self.username = username
self.email = email
self.password_hash = None
# User management (appropriate)
def set_password(self, password):
self.password_hash = hash(password)
# Email functionality (should be separate class)
def send_welcome_email(self):
print(f"Sending welcome email to {self.email}")
# File operations (should be separate class)
def save_to_file(self, filename):
with open(filename, 'w') as f:
f.write(f"{self.username},{self.email}")
# Database operations (should be separate class)
def save_to_database(self, db_connection):
# Database save logic here
pass
# Logging (should be separate class)
def log_login_attempt(self, success):
print(f"Login attempt for {self.username}: {'SUCCESS' if success else 'FAILED'}")
Better approach with high cohesion:
class User:
"""High cohesion - focused on user data and basic operations"""
def __init__(self, username, email):
self.username = username
self.email = email
self.password_hash = None
def set_password(self, password):
self.password_hash = hash(password)
def check_password(self, password):
return self.password_hash == hash(password)
class EmailService:
"""Focused on email operations"""
def send_welcome_email(self, user):
print(f"Sending welcome email to {user.email}")
class UserRepository:
"""Focused on user data persistence"""
def save_user(self, user, filename):
with open(filename, 'w') as f:
f.write(f"{user.username},{user.email}")
class LoginLogger:
"""Focused on login tracking"""
def log_attempt(self, username, success):
print(f"Login attempt for {username}: {'SUCCESS' if success else 'FAILED'}")
Practice¶
Exercise 1: Create a basic class
Create a Book class with the following requirements:
-
Constructor takes title, author, ISBN, and number of pages
-
Method to check if the book is long (more than 300 pages)
-
Method to get a summary string
-
Method to check if it matches a search term (in title or author)
-
Proper validation in the constructor
Task: Implement the Book class with all methods and test it.
Sample Solution
class Book:
"""Represents a book with basic information and operations"""
def __init__(self, title, author, isbn, pages):
"""
Initialize a new book
Args:
title (str): Book title
author (str): Author name
isbn (str): ISBN number
pages (int): Number of pages
"""
# Validation
if not title or not title.strip():
raise ValueError("Title cannot be empty")
if not author or not author.strip():
raise ValueError("Author cannot be empty")
if not isbn or not isbn.strip():
raise ValueError("ISBN cannot be empty")
if pages <= 0:
raise ValueError("Pages must be positive")
# Set attributes
self.title = title.strip()
self.author = author.strip()
self.isbn = isbn.strip()
self.pages = pages
def is_long(self):
"""Check if book is long (more than 300 pages)"""
return self.pages > 300
def get_summary(self):
"""Get a summary string of the book"""
length_desc = "long" if self.is_long() else "standard"
return f"'{self.title}' by {self.author} ({self.pages} pages, {length_desc})"
def matches_search(self, search_term):
"""Check if search term appears in title or author"""
search_lower = search_term.lower()
return (search_lower in self.title.lower() or
search_lower in self.author.lower())
def __str__(self):
return f"Book({self.title}, {self.author})"
# Test the class
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", "978-0-7432-7356-5", 180)
book2 = Book("War and Peace", "Leo Tolstoy", "978-0-14-044793-4", 1225)
print(book1.get_summary()) # 'The Great Gatsby' by F. Scott Fitzgerald (180 pages, standard)
print(book2.is_long()) # True
print(book1.matches_search("gatsby")) # True
print(book2.matches_search("Tolstoy")) # True
Exercise 2: Composition design
Design a Computer class using composition with these components:
-
Processor(brand, speed, cores) -
Memory(capacity in GB, type like “DDR4”) -
Storage(capacity in GB, type like “SSD” or “HDD”)
The Computer should be able to:
-
Start up (requires all components)
-
Get system specs
-
Check if it can run a program (needs minimum processor speed and memory)
Task: Implement all classes using composition principles.
Sample Solution
class Processor:
"""CPU component"""
def __init__(self, brand, speed_ghz, cores):
self.brand = brand
self.speed_ghz = speed_ghz
self.cores = cores
def get_specs(self):
return f"{self.brand} {self.speed_ghz}GHz {self.cores}-core"
def can_handle_load(self, required_speed):
return self.speed_ghz >= required_speed
class Memory:
"""RAM component"""
def __init__(self, capacity_gb, memory_type):
self.capacity_gb = capacity_gb
self.memory_type = memory_type
def get_specs(self):
return f"{self.capacity_gb}GB {self.memory_type}"
def can_handle_load(self, required_gb):
return self.capacity_gb >= required_gb
class Storage:
"""Storage component"""
def __init__(self, capacity_gb, storage_type):
self.capacity_gb = capacity_gb
self.storage_type = storage_type
def get_specs(self):
return f"{self.capacity_gb}GB {self.storage_type}"
class Computer:
"""Computer built using composition"""
def __init__(self, brand, model, processor, memory, storage):
self.brand = brand
self.model = model
self.processor = processor # Composition
self.memory = memory # Composition
self.storage = storage # Composition
self.is_running = False
def start_up(self):
"""Start the computer"""
if not self.is_running:
self.is_running = True
return f"{self.brand} {self.model} started successfully"
return "Computer already running"
def shutdown(self):
"""Shutdown the computer"""
if self.is_running:
self.is_running = False
return f"{self.brand} {self.model} shut down"
return "Computer already off"
def get_system_specs(self):
"""Get complete system specifications"""
specs = [
f"Computer: {self.brand} {self.model}",
f"Processor: {self.processor.get_specs()}",
f"Memory: {self.memory.get_specs()}",
f"Storage: {self.storage.get_specs()}"
]
return "\n".join(specs)
def can_run_program(self, program_name, min_speed_ghz, min_memory_gb):
"""Check if computer meets program requirements"""
cpu_ok = self.processor.can_handle_load(min_speed_ghz)
memory_ok = self.memory.can_handle_load(min_memory_gb)
if cpu_ok and memory_ok:
return f"✓ Can run {program_name}"
else:
issues = []
if not cpu_ok:
issues.append(f"CPU too slow (need {min_speed_ghz}GHz, have {self.processor.speed_ghz}GHz)")
if not memory_ok:
issues.append(f"Not enough RAM (need {min_memory_gb}GB, have {self.memory.capacity_gb}GB)")
return f"✗ Cannot run {program_name}: {', '.join(issues)}"
# Create components
intel_cpu = Processor("Intel i7", 3.2, 8)
ddr4_ram = Memory(16, "DDR4")
ssd_storage = Storage(512, "SSD")
# Create computer using composition
gaming_pc = Computer("ASUS", "ROG Strix", intel_cpu, ddr4_ram, ssd_storage)
print(gaming_pc.start_up())
print(gaming_pc.get_system_specs())
print(gaming_pc.can_run_program("Photoshop", 2.5, 8)) # Should pass
print(gaming_pc.can_run_program("Video Editor", 4.0, 32)) # Should fail
Exercise 3: Refactor inheritance to composition
You have this inheritance-based design. Refactor it to use composition instead:
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
self.speed = 0
class Car(Vehicle):
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
def accelerate(self):
self.speed += 10
class Motorcycle(Vehicle):
def __init__(self, make, model, engine_size):
super().__init__(make, model)
self.engine_size = engine_size
def accelerate(self):
self.speed += 20
Task: Redesign using composition with separate components for different aspects.
Sample Solution
class VehicleInfo:
"""Component for basic vehicle information"""
def __init__(self, make, model, vehicle_type):
self.make = make
self.model = model
self.vehicle_type = vehicle_type
def get_description(self):
return f"{self.make} {self.model} ({self.vehicle_type})"
class MotionSystem:
"""Component for movement and acceleration"""
def __init__(self, acceleration_rate):
self.speed = 0
self.acceleration_rate = acceleration_rate
def accelerate(self):
self.speed += self.acceleration_rate
return f"Accelerating to {self.speed} km/h"
def brake(self, amount):
self.speed = max(0, self.speed - amount)
return f"Slowing to {self.speed} km/h"
def get_speed(self):
return self.speed
class CarFeatures:
"""Component for car-specific features"""
def __init__(self, doors):
self.doors = doors
def get_info(self):
return f"{self.doors} doors"
class MotorcycleFeatures:
"""Component for motorcycle-specific features"""
def __init__(self, engine_size):
self.engine_size = engine_size
def get_info(self):
return f"{self.engine_size}cc engine"
class Vehicle:
"""Base vehicle using composition"""
def __init__(self, vehicle_info, motion_system):
self.info = vehicle_info # Composition
self.motion = motion_system # Composition
def accelerate(self):
return self.motion.accelerate()
def brake(self, amount=10):
return self.motion.brake(amount)
def get_description(self):
return self.info.get_description()
def get_speed(self):
return self.motion.get_speed()
class Car(Vehicle):
"""Car using composition instead of inheritance"""
def __init__(self, make, model, doors):
vehicle_info = VehicleInfo(make, model, "Car")
motion_system = MotionSystem(10) # Cars accelerate by 10 km/h
super().__init__(vehicle_info, motion_system)
self.features = CarFeatures(doors) # Car-specific composition
def get_full_description(self):
return f"{self.get_description()} - {self.features.get_info()}"
class Motorcycle(Vehicle):
"""Motorcycle using composition instead of inheritance"""
def __init__(self, make, model, engine_size):
vehicle_info = VehicleInfo(make, model, "Motorcycle")
motion_system = MotionSystem(20) # Motorcycles accelerate by 20 km/h
super().__init__(vehicle_info, motion_system)
self.features = MotorcycleFeatures(engine_size) # Motorcycle-specific composition
def get_full_description(self):
return f"{self.get_description()} - {self.features.get_info()}"
# Usage
car = Car("Toyota", "Camry", 4)
motorcycle = Motorcycle("Honda", "CBR", 600)
print(car.accelerate()) # Accelerating to 10 km/h
print(motorcycle.accelerate()) # Accelerating to 20 km/h
print(car.get_full_description()) # Toyota Camry (Car) - 4 doors
print(motorcycle.get_full_description()) # Honda CBR (Motorcycle) - 600cc engine
# Benefits of composition:
# 1. Can change motion system at runtime
# 2. Can reuse MotionSystem in other contexts
# 3. Easier to test components separately
# 4. More flexible than inheritance hierarchy
Recap¶
-
Classes are blueprints that define object structure and behavior through attributes and methods
-
Constructors (
__init__) set up initial object state and should include proper validation -
Methods define what objects can do and should follow single responsibility principle
-
Composition builds complex objects from simpler components, providing more flexibility than inheritance
-
“Composition over inheritance“ leads to more maintainable and testable code
-
Cohesive classes have a single, well-defined purpose with all methods supporting that purpose
-
Good class design emphasizes clear responsibilities, meaningful names, and proper encapsulation
Building classes with composition creates more flexible, maintainable systems where components can be developed, tested, and modified independently while still working together to create powerful functionality.