KAHIBARO
Discord Login Register

4.12 Inheritance and Composition

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:

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.

Example:

python
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:

python
dog = Dog("Buddy")
cat = Cat("Misty")
print(dog.name)        # Inherited attribute
print(dog.speak())     # "Woof!"
print(cat.speak())     # "Meow"

Here:

“Is a” Relationship

Use inheritance when you can clearly say:

ChildClass is a ParentClass.

Examples that make sense:

Examples that do not make sense:

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

python
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

python
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:

python
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:

python
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:

  1. Tight coupling
    The child class depends heavily on the parent. If the parent changes, many children may break.
  2. Inflexible structure
    A class can only inherit from one main parent (in many languages) and getting behavior from multiple sources becomes complex.
  3. Wrong relationships
    Developers sometimes use inheritance just to reuse code even when “is a” is not true.
  4. Deep inheritance trees
    When you have ClassA -> ClassB -> ClassC -> ClassD, it becomes hard to understand where behavior comes from.

Example of a bad inheritance design:

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

Example:

python
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:

python
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:

ClassA has a ClassB.

Examples that make sense:

Examples:

python
class Customer:
    def __init__(self, name):
        self.name = name
class Order:
    def __init__(self, order_id, customer):
        self.order_id = order_id
        self.customer = customer

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

python
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

python
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

python
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:

Example of flexible composition:

python
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 order

Usage:

python
service1 = OrderService(ConsoleLogger())
service2 = OrderService(FileLogger("orders.log"))

Same OrderService code, different logger implementations. No inheritance needed.


Inheritance vs Composition

Comparing the Two

AspectInheritanceComposition
Relationship type“is a”“has a”
Code reuse styleShare and override parent behaviorCombine behaviors of separate objects
CouplingTighter coupling to parentLooser coupling, more flexible
FlexibilityHarder to change parent without side effectsEasier to swap or modify components
Typical use caseVariants of the same conceptBuilding complex objects from simple parts
Number of parentsUsually one main parentMany components can be used together
RiskMisused for code reuse, deep hierarchiesMore 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)

python
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:

Using Composition (more flexible)

python
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:

python
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:

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:

Example:

python
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 product

Composition lets you plug in different implementations:

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:

python
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:

Examples:

  1. Different types of payments:
python
   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
  1. 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:

python
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:

python
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:

python
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:

python
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

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!