4.12 Inheritance and Composition
Table of Contents
Why Inheritance and Composition Matter
When you work with object oriented programming, you often need to connect classes so they can reuse code and work together. Two major ways to do this are inheritance and composition.
Both are about reusing behavior and structuring your code, but they do it in very different ways:
- Inheritance: “is a” relationship
Example:Dogis aAnimal - Composition: “has a” relationship
Example:Carhas aEngine
Understanding the difference and when to use each will make your backend code cleaner, easier to change, and less buggy.
Key rule: Prefer composition over inheritance for flexibility. Use inheritance only when there is a clear, stable “is a” relationship.
We will use simple pseudo code that looks similar to Python or other C-like languages. The ideas are the same in most languages.
Inheritance
Basic Idea of Inheritance
With inheritance, you create a base class (also called parent or superclass) and then create child classes (subclasses) that extend it.
- The child class gets all the attributes and methods from the parent class.
- The child can add new behavior or override existing behavior.
Example:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some generic animal sound"
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow"Usage:
dog = Dog("Buddy")
cat = Cat("Misty")
print(dog.name) # Inherited attribute
print(dog.speak()) # "Woof!"
print(cat.speak()) # "Meow"Here:
DogandCatinheritnameand__init__fromAnimal.- They override
speak.
“Is a” Relationship
Use inheritance when you can clearly say:
ChildClassis aParentClass.
Examples that make sense:
Dogis anAnimalAdminUseris aUserPremiumAccountis anAccount
Examples that do not make sense:
Caris aEngine(wrong, car has an engine)Orderis aDatabaseConnection(wrong, order uses a database connection)
Rule: Only use inheritance when the subclass is a specialized version of the parent, not just because it needs similar behavior.
Why Inheritance Can Be Useful
Some typical backend examples:
Example 1: Common API Response Base
class ApiResponse:
def __init__(self, success, message):
self.success = success
self.message = message
class DataResponse(ApiResponse):
def __init__(self, data):
super().__init__(success=True, message="OK")
self.data = data
class ErrorResponse(ApiResponse):
def __init__(self, error_code, message):
super().__init__(success=False, message=message)
self.error_code = error_code
You reuse the shared fields success and message and specialize with data or error_code.
Example 2: Different Types of Users
class User:
def __init__(self, email):
self.email = email
def can_manage_system(self):
return False
class AdminUser(User):
def can_manage_system(self):
return True
AdminUser is still a User, but with different permissions.
Overriding Methods
A subclass can replace behavior:
class Payment:
def process(self, amount):
print("Processing generic payment of", amount)
class CreditCardPayment(Payment):
def process(self, amount):
print("Charging credit card for", amount)
class PayPalPayment(Payment):
def process(self, amount):
print("Charging PayPal for", amount)Usage:
payments = [
CreditCardPayment(),
PayPalPayment(),
]
for p in payments:
p.process(100)
This is polymorphism in action, but the important part here is that inheritance lets each subclass define its own process while still sharing the same interface.
Risks and Problems with Inheritance
Inheritance looks attractive, but it can cause issues:
- Tight coupling
The child class depends heavily on the parent. If the parent changes, many children may break. - Inflexible structure
A class can only inherit from one main parent (in many languages) and getting behavior from multiple sources becomes complex. - Wrong relationships
Developers sometimes use inheritance just to reuse code even when “is a” is not true. - Deep inheritance trees
When you haveClassA -> ClassB -> ClassC -> ClassD, it becomes hard to understand where behavior comes from.
Example of a bad inheritance design:
class DatabaseLogger:
def log(self, message):
print("Saving to DB:", message)
class User(DatabaseLogger):
def __init__(self, email):
self.email = email
User is not a DatabaseLogger. It might use logging, but it should not be a logger. This is a hint that another pattern, usually composition, is better.
Composition
Basic Idea of Composition
With composition, you build objects by combining other objects as attributes. Instead of “is a”, you create “has a” relationships.
- A class can contain instances of other classes.
- You can change parts easily, since they are separate components.
Example:
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self, engine):
self.engine = engine
def drive(self):
self.engine.start()
print("Car is moving")Usage:
engine = Engine()
car = Car(engine)
car.drive()
Car has an Engine. That matches the real world and is easy to understand.
“Has a” Relationship
Use composition when you can say:
ClassAhas aClassB.
Examples that make sense:
Orderhas aCustomerCarthasCartItemsUserServicehas aUserRepositoryEmailSenderhas anSmtpClient
Examples:
class Customer:
def __init__(self, name):
self.name = name
class Order:
def __init__(self, order_id, customer):
self.order_id = order_id
self.customer = customerHere the order object keeps a customer object inside it.
Composition in Backend Code
Example 1: Service Uses Repository
In backend applications you often separate business logic from data access.
class UserRepository:
def find_by_email(self, email):
# query DB
pass
def save(self, user):
# insert or update in DB
pass
class UserService:
def __init__(self, user_repository):
self.user_repository = user_repository # composition
def register_user(self, email, password):
existing = self.user_repository.find_by_email(email)
if existing:
raise Exception("User already exists")
# create and save new user
self.user_repository.save({"email": email, "password": password})
UserService has a UserRepository. It does not inherit from it.
Example 2: Controller Uses Service
class UserController:
def __init__(self, user_service):
self.user_service = user_service
def post_register(self, request_body):
email = request_body["email"]
password = request_body["password"]
self.user_service.register_user(email, password)
return {"status": "ok"}
UserController has a UserService. This is composition again.
Example 3: Logger as a Component
class Logger:
def info(self, message):
print("[INFO]", message)
class PaymentService:
def __init__(self, logger):
self.logger = logger
def create_payment(self, amount):
self.logger.info(f"Creating payment for {amount}")
# business logic
PaymentService has a Logger. You can swap the logger later with a different implementation if needed.
Why Composition Is Usually Better
Composition provides:
- Flexibility
You can change combined parts without touching other code. - Looser coupling
Components depend on interfaces, not concrete parent classes. - Reusability
A component (for exampleLogger) can be used by many other classes. - Simpler structure
You avoid deep inheritance trees and keep class relationships clearer.
Example of flexible composition:
class ConsoleLogger:
def info(self, message):
print("[INFO]", message)
class FileLogger:
def __init__(self, file_path):
self.file_path = file_path
def info(self, message):
with open(self.file_path, "a") as f:
f.write("[INFO] " + message + "\n")
class OrderService:
def __init__(self, logger):
self.logger = logger
def create_order(self, data):
self.logger.info("Creating order")
# create orderUsage:
service1 = OrderService(ConsoleLogger())
service2 = OrderService(FileLogger("orders.log"))
Same OrderService code, different logger implementations. No inheritance needed.
Inheritance vs Composition
Comparing the Two
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship type | “is a” | “has a” |
| Code reuse style | Share and override parent behavior | Combine behaviors of separate objects |
| Coupling | Tighter coupling to parent | Looser coupling, more flexible |
| Flexibility | Harder to change parent without side effects | Easier to swap or modify components |
| Typical use case | Variants of the same concept | Building complex objects from simple parts |
| Number of parents | Usually one main parent | Many components can be used together |
| Risk | Misused for code reuse, deep hierarchies | More objects to manage, but clearer structure |
Examples: Same Problem, Two Approaches
Scenario: Users with Notifications
You need users that can receive notifications by email and maybe later by SMS.
Using Inheritance (less ideal here)
class User:
def __init__(self, email):
self.email = email
def notify(self, message):
print(f"Sending email to {self.email}: {message}")
class SmsUser(User):
def __init__(self, email, phone):
super().__init__(email)
self.phone = phone
def notify(self, message):
print(f"Sending SMS to {self.phone}: {message}")Problems:
- What if you want SMS and email?
- Do you create another subclass?
EmailAndSmsUser? - The number of subclasses grows quickly.
Using Composition (more flexible)
class EmailNotifier:
def notify(self, user, message):
print(f"Email to {user.email}: {message}")
class SmsNotifier:
def notify(self, user, message):
print(f"SMS to {user.phone}: {message}")
class User:
def __init__(self, email, phone=None, notifiers=None):
self.email = email
self.phone = phone
self.notifiers = notifiers or []
def send_notifications(self, message):
for notifier in self.notifiers:
notifier.notify(self, message)Usage:
email_user = User(
email="a@example.com",
notifiers=[EmailNotifier()]
)
sms_and_email_user = User(
email="b@example.com",
phone="+123",
notifiers=[EmailNotifier(), SmsNotifier()]
)
email_user.send_notifications("Welcome!")
sms_and_email_user.send_notifications("Welcome!")Here:
Userhas a list of notifiers.- You can add or remove notifier types without creating new subclasses.
Common Patterns in Backend with Composition
Services, Repositories, and Clients
Backend applications often follow patterns that rely heavily on composition.
Typical stack for a feature:
ControllerorRouteruses aServiceServiceuses one or moreRepositoriesServicemay also use external APIClients- Components may use a shared
Logger,Cache, orConfig
Example:
class CacheClient:
def get(self, key):
pass
def set(self, key, value):
pass
class ProductRepository:
def __init__(self, db_connection):
self.db = db_connection
def find_by_id(self, product_id):
# query DB
pass
class ProductService:
def __init__(self, repository, cache):
self.repository = repository
self.cache = cache
def get_product(self, product_id):
cached = self.cache.get(product_id)
if cached:
return cached
product = self.repository.find_by_id(product_id)
if product:
self.cache.set(product_id, product)
return productComposition lets you plug in different implementations:
- A
MemoryCacheClientorRedisCacheClient - A
MockProductRepositoryfor tests
All without inheritance.
Adapter Style Classes
Sometimes you need to make two parts of your code work together.
Example: wrapping a low-level HTTP client:
class HttpClient:
def get(self, url):
# low level HTTP call
pass
class WeatherApiClient:
def __init__(self, http_client):
self.http_client = http_client
def get_forecast(self, city):
url = f"https://example.com/weather?city={city}"
response = self.http_client.get(url)
# parse response
return response
WeatherApiClient has an HttpClient. This is composition again.
When to Choose Inheritance
Even though composition is usually preferred, there are still valid cases for inheritance.
Use inheritance when:
- You really have a hierarchy of types.
- You want polymorphic behavior where each subclass behaves differently but shares the same interface.
- The base class defines a contract or template that subclasses fill in.
Examples:
- Different types of payments:
class Payment:
def process(self, amount):
raise NotImplementedError
class CreditCardPayment(Payment):
def process(self, amount):
# process card
class BankTransferPayment(Payment):
def process(self, amount):
# process bank transfer- Different types of queries in a query builder, or different shapes for error types that share a base error.
Guideline:
Use inheritance for type hierarchies and polymorphism,
use composition to combine behaviors and build larger objects from smaller pieces.
Practical Exercises to Cement Understanding
Try these small exercises mentally or in your language of choice.
Exercise 1: Fix Bad Inheritance
Given:
class DatabaseConnection:
def connect(self):
print("Connecting to DB")
class User(DatabaseConnection):
def __init__(self, email):
self.email = email
Question:
Refactor this using composition instead of inheritance.
Possible solution:
class DatabaseConnection:
def connect(self):
print("Connecting to DB")
class UserRepository:
def __init__(self, db_connection):
self.db_connection = db_connection
def find_by_email(self, email):
self.db_connection.connect()
# query DB
return {"email": email}
The User object should not extend DatabaseConnection. A separate UserRepository that has a DatabaseConnection is better.
Exercise 2: Add Behavior With Composition
You have:
class Order:
def __init__(self, items):
self.items = items # list of (name, price)
Add a separate component that can calculate the total price, and use composition instead of adding all logic inside Order.
Possible solution:
class PriceCalculator:
def calculate_total(self, items):
return sum(price for name, price in items)
class Order:
def __init__(self, items, calculator):
self.items = items
self.calculator = calculator
def total(self):
return self.calculator.calculate_total(self.items)Summary
- Inheritance lets one class extend another. Use it when there is a clear “is a” relationship and you need polymorphic behavior.
- Composition builds a class by combining other classes. Use it when there is a “has a” relationship or when you want flexible code reuse.
- Backend code, especially services, repositories, controllers, and clients, usually benefits more from composition than from complex inheritance trees.
- By choosing the right relationship between your classes, you make your backend code easier to understand, test, and change.
Views: 7
KAHIBARO