Object-Oriented Programming (Java/Python)

Chapter 5
Object Relationships, Composition, and Inheritance

Bachelors Program in Full Stack Development
Object-Oriented Programming (Java/Python)   •   Bachelor

Object Relationships, Composition, and Inheritance

Introduction

Object-oriented programming (OOP) models software as a collection of interacting objects. An object combines state, represented by attributes, with behavior, represented by methods. However, useful systems are rarely made of isolated objects. A Customer places an Order, an Order contains OrderLine objects, a PaymentService uses a payment gateway, and a SavingsAccount is a specialized kind of Account. These connections are called object relationships.

Understanding relationships is central to designing maintainable Java and Python applications. A relationship determines which object knows about another object, which object controls another object's lifetime, how responsibilities are distributed, and how changes propagate through a system. Poorly selected relationships lead to rigid code, excessive coupling, and fragile inheritance hierarchies. Well-selected relationships make software easier to test, extend, and understand.

This chapter examines association, dependency, aggregation, composition, and inheritance. It also distinguishes “is-a” relationships from “has-a” relationships, explains method overriding and polymorphism, and develops the design principle that composition should generally be preferred over inheritance when the domain does not require a genuine subtype relationship.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain association, dependency, aggregation, composition, and inheritance;
  • distinguish “is-a” from “has-a” relationships;
  • model object relationships using UML-style multiplicity and clear ownership rules;
  • implement composition and inheritance in Java and Python;
  • apply method overriding and runtime polymorphism correctly;
  • select composition, inheritance, or delegation based on design evidence;
  • identify coupling, cohesion, substitutability, and lifecycle concerns;
  • evaluate an object-oriented design in a realistic application scenario.

1. Relationships as Design Decisions

A relationship is more than a line between two classes. It expresses a design decision about knowledge, collaboration, ownership, and substitutability.

Consider a simplified online bookstore:

  • A Customer may place many Order objects.
  • An Order uses a PaymentMethod.
  • An Order is composed of OrderLine objects.
  • A CreditCardPayment may be a kind of PaymentMethod.

These statements have different meanings. “A customer places an order” describes an association. “An order uses a payment method” may be a dependency or association. “An order is made up of order lines” commonly suggests composition. “A credit-card payment is a payment method” suggests inheritance or, in a more flexible design, implementation of an interface.

A useful analysis asks four questions:

  1. Visibility: Does one object need a reference to another?
  2. Duration: How long does the relationship last?
  3. Ownership: Does one object control the other's existence?
  4. Substitutability: Can one object safely be used wherever another is expected?

These questions prevent relationships from being selected merely because they appear convenient in code.

2. Association

An association is a structural relationship in which objects of one class are connected to objects of another class. The connection may be one-way or two-way and may be temporary or persistent.

For example, a Doctor treats Patient objects. The doctor and patient are independent entities: deleting a doctor record does not conceptually destroy the patient. The association can therefore be modeled with references without ownership.

class Patient:
    def __init__(self, name):
        self.name = name


class Doctor:
    def __init__(self, name):
        self.name = name
        self.patients = []

    def register_patient(self, patient):
        if patient not in self.patients:
            self.patients.append(patient)

In Java:

import java.util.ArrayList;
import java.util.List;

class Doctor {
    private final String name;
    private final List<Patient> patients = new ArrayList<>();

    Doctor(String name) {
        this.name = name;
    }

    void registerPatient(Patient patient) {
        if (!patients.contains(patient)) {
            patients.add(patient);
        }
    }
}

Association has several possible multiplicities:

MultiplicityMeaning
1Exactly one object
0..1Zero or one object
or 0..Any number, including none
1..*One or more objects
m..nBetween m and n objects, inclusive

Multiplicity is a domain constraint, not merely a notation detail. If each order must belong to exactly one customer, the program should enforce that rule through constructors, validation, or database constraints.

3. Dependency

A dependency is a weaker relationship in which one class temporarily uses another. The supplier may appear as a method parameter, local variable, return type, or service invoked during an operation. The client does not necessarily retain the supplier as part of its long-term state.

class ReportPrinter:
    def print_report(self, report, formatter):
        text = formatter.format(report)
        print(text)

ReportPrinter depends on formatter during print_report, but it need not own or permanently store the formatter. In Java:

class ReportPrinter {
    void printReport(Report report, Formatter formatter) {
        String text = formatter.format(report);
        System.out.println(text);
    }
}

Dependency injection makes such dependencies explicit and testable. A production formatter can be replaced with a test double without changing ReportPrinter.

class CheckoutService:
    def __init__(self, payment_gateway):
        self.payment_gateway = payment_gateway

    def checkout(self, amount):
        return self.payment_gateway.charge(amount)

Although the gateway is stored as an attribute here, the important design idea is that the service receives an abstraction from outside rather than constructing a particular gateway internally. This reduces coupling and supports testing.

4. Aggregation and Composition

Aggregation and composition are specialized forms of whole–part association. Both describe a “has-a” relationship, but they differ in ownership and lifecycle.

Aggregation

Aggregation is a weak whole–part relationship. The whole refers to parts, but the parts can exist independently and may be shared by more than one whole.

A Department may aggregate Employee objects. Employees can exist if a department is closed, and an employee may transfer to another department.

class Department:
    def __init__(self, name, employees=None):
        self.name = name
        self.employees = list(employees or [])

The department organizes employees but does not necessarily create or destroy them. In practical programming, aggregation is often represented by an ordinary collection reference. The distinction is primarily semantic and depends on lifecycle rules.

Composition

Composition is a strong whole–part relationship. The whole owns the parts, and the parts normally have no meaningful independent existence outside that whole. A part belongs to one composite at a time, and its lifecycle is controlled by the composite.

An Order and its OrderLine objects provide a common example. An order line represents a product entry within a particular order. If the order is removed from the system, its order lines are normally removed as well.

class OrderLine:
    def __init__(self, product_id, quantity, unit_price):
        if quantity <= 0:
            raise ValueError("quantity must be positive")
        if unit_price < 0:
            raise ValueError("unit_price cannot be negative")
        self.product_id = product_id
        self.quantity = quantity
        self.unit_price = unit_price

    def subtotal(self):
        return self.quantity * self.unit_price


class Order:
    def __init__(self, order_id):
        self.order_id = order_id
        self._lines = []

    def add_line(self, product_id, quantity, unit_price):
        self._lines.append(OrderLine(product_id, quantity, unit_price))

    def total(self):
        return sum(line.subtotal() for line in self._lines)

Here, Order creates its own OrderLine instances. External code receives no direct mutable list, which helps preserve the invariant that order lines belong to their order.

In Java:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

class OrderLine {
    private final String productId;
    private final int quantity;
    private final BigDecimal unitPrice;

    OrderLine(String productId, int quantity, BigDecimal unitPrice) {
        if (quantity <= 0 || unitPrice.signum() < 0) {
            throw new IllegalArgumentException("Invalid line values");
        }
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    BigDecimal subtotal() {
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
}

class Order {
    private final List<OrderLine> lines = new ArrayList<>();

    void addLine(String productId, int quantity, BigDecimal unitPrice) {
        lines.add(new OrderLine(productId, quantity, unitPrice));
    }
}

The garbage collector in Java and Python manages memory, but garbage collection is not identical to domain ownership. A part may be eligible for memory reclamation when no references remain, yet the business model may require explicit deletion, archiving, or persistence rules. Composition is therefore a conceptual lifecycle relationship, not simply a memory-management mechanism.

5. “Is-A” and “Has-A”

An is-a relationship expresses specialization or substitutability. A SavingsAccount is an Account if every SavingsAccount can validly be treated as an Account.

A has-a relationship expresses containment, collaboration, or assembly. A Car has an Engine; it is not an engine. A CheckoutService has a payment gateway; it is not a payment gateway.

QuestionLikely relationshipTypical implementation
Is the subtype valid wherever the base type is expected?Is-aInheritance or interface implementation
Does one object use another to perform work?Has-a/dependencyParameter, field, or delegation
Does the whole control the part's lifecycle?Has-a/compositionOwned field or collection
Can the part exist independently?Has-a/aggregationExternal reference or collection

A common error is to use inheritance for code reuse alone. Shared code does not prove an is-a relationship. If Report and Invoice both format dates, they do not necessarily belong in an inheritance hierarchy. A utility, value object, or collaborator may be more appropriate.

6. Inheritance and Substitutability

Inheritance allows a class to derive from another class. The derived class receives accessible behavior and may add or specialize behavior. In Java, a class can extend one class; in Python, a class can inherit from one or more classes.

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("insufficient funds")
        self.balance -= amount


class SavingsAccount(Account):
    def withdraw(self, amount):
        if amount > self.balance - 100:
            raise ValueError("minimum balance would be violated")
        self.balance -= amount

SavingsAccount overrides withdraw. The override changes the behavioral rule, so it must still satisfy the expectations of code using an Account. If client code assumes that any account can withdraw up to its available balance, this subtype may violate that assumption. This illustrates the Liskov Substitution Principle (LSP): a subtype should be usable wherever its base type is expected without breaking correctness.

A stronger example is a FixedDepositAccount that does not permit withdrawal at all. Although it may appear financially related to Account, forcing it into an interface promising withdrawal could create a misleading hierarchy. The abstraction should represent behavior genuinely shared by all subtypes.

Java overriding

class Notification {
    void send(String message) {
        System.out.println("Generic notification: " + message);
    }
}

class EmailNotification extends Notification {
    @Override
    void send(String message) {
        System.out.println("Sending email: " + message);
    }
}

@Override asks the compiler to verify that the method actually overrides a superclass method. In Java, overriding depends on a compatible method signature. Static methods are hidden rather than overridden, and private methods are not overridden because they are not inherited by subclasses in the usual polymorphic sense.

Python overriding

class Notification:
    def send(self, message):
        print(f"Generic notification: {message}")


class EmailNotification(Notification):
    def send(self, message):
        print(f"Sending email: {message}")

Python uses dynamic dispatch: when notification.send(...) is called, the implementation is selected according to the object's runtime type. super() can invoke inherited behavior when extension rather than complete replacement is intended.

class AuditedEmail(EmailNotification):
    def send(self, message):
        print("Audit: email requested")
        super().send(message)

Overriding should preserve the method's conceptual contract. It should not unexpectedly strengthen preconditions, weaken important postconditions, or change the meaning of established operations.

7. Polymorphism and Abstract Types

Polymorphism allows one interface or base type to represent objects with different implementations.

class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError


class CardPayment(PaymentMethod):
    def pay(self, amount):
        return f"Charged card: {amount}"


class WalletPayment(PaymentMethod):
    def pay(self, amount):
        return f"Charged wallet: {amount}"


def complete_payment(method: PaymentMethod, amount):
    return method.pay(amount)

The function depends on the operation pay, not on a particular payment implementation. In Java, an interface is often the clearest abstraction:

interface PaymentMethod {
    void pay(int cents);
}

class CardPayment implements PaymentMethod {
    @Override
    public void pay(int cents) {
        System.out.println("Charging card");
    }
}

Interfaces are particularly useful when unrelated classes share a capability but not a common implementation history. In Python, protocols or abstract base classes can express similar expectations, while duck typing may be sufficient in smaller designs.

8. Why Composition Is Often Preferred

The principle “favor composition over inheritance” does not mean inheritance is bad. It means that assembling behavior from collaborators is often safer than creating a deep, rigid hierarchy.

Composition offers several advantages:

  • Lower coupling: The host depends on a collaborator's contract rather than inherited implementation details.
  • Replaceability: A component can be exchanged at runtime or during testing.
  • Localized change: Changing one strategy need not affect a superclass and many subclasses.
  • Multiple behaviors: An object can combine several capabilities without multiple inheritance.
  • Improved testing: Dependencies can be replaced by fakes, stubs, or mocks.

A tax calculator illustrates the strategy pattern:

class StandardTax:
    def calculate(self, subtotal):
        return subtotal * 0.20


class ReducedTax:
    def calculate(self, subtotal):
        return subtotal * 0.05


class Invoice:
    def __init__(self, tax_policy):
        self.tax_policy = tax_policy

    def total(self, subtotal):
        return subtotal + self.tax_policy.calculate(subtotal)

Invoice has a tax policy. It does not need subclasses such as StandardTaxInvoice and ReducedTaxInvoice. New policies can be added without modifying the invoice's core structure.

Inheritance remains appropriate when:

  • the subtype genuinely satisfies the base type's contract;
  • the hierarchy is shallow and conceptually stable;
  • polymorphic substitution is required;
  • common state and behavior are meaningful, not merely convenient;
  • the base abstraction is designed for extension.

Composition is preferable when behavior changes independently, when multiple combinations are needed, or when the proposed inheritance expresses only code reuse.

9. Worked Design Example: A Learning Platform

Consider an illustrative case: a university is developing a learning platform. A course contains lessons, students enroll in courses, and the platform sends notifications.

Background and problem

The initial design creates subclasses EmailCourse, SMSCourse, and PushCourse to represent notification choices. It also makes Lesson inherit from Course because both have titles and descriptions. The system is difficult to extend and changes to notification behavior require new subclasses.

Analysis

Lesson is not a course; it is a part of a course. This is a composition relationship. A student enrolls in a course, so that is an association. Notification delivery is a variable behavior and should be modeled as a collaborator. Email, SMS, and push delivery may implement a common notification interface, but a course should not inherit from them.

Improved approach

class Lesson:
    def __init__(self, title, content):
        self.title = title
        self.content = content


class Course:
    def __init__(self, code, title, notifier):
        self.code = code
        self.title = title
        self._lessons = []
        self.notifier = notifier

    def add_lesson(self, title, content):
        self._lessons.append(Lesson(title, content))

    def publish(self):
        self.notifier.send(f"Course {self.code} is now available")

Course composes lessons and has a notifier. A Student can be associated with many courses independently:

class Student:
    def __init__(self, student_id):
        self.student_id = student_id
        self.courses = set()

    def enroll(self, course):
        self.courses.add(course)

Outcome

The improved design permits a course to use email today and push notifications tomorrow. A fake notifier can be injected into tests. Lessons are created through the course, and the model clearly communicates their lifecycle. Students remain independent of courses, supporting withdrawal and transfer without destroying either object.

Learning points

The case demonstrates four decisions: use composition for owned parts, association for independently existing participants, dependency injection for variable services, and inheritance only where substitutability is defensible.

10. Design Risks and Best Practices

Excessive inheritance

Deep hierarchies are difficult to understand because behavior may be distributed across many ancestors. A change in a base class can affect distant subclasses. Prefer shallow hierarchies and stable abstractions.

Inheritance for convenience

If a class inherits only to reuse two methods, the relationship may be false. Extract shared behavior into a collaborator, utility, or value object.

Bidirectional associations

Bidirectional references can become inconsistent. If Department stores employees and each Employee stores a department, both sides must be updated together. Encapsulate updates in methods such as assign_employee rather than exposing mutable collections.

Hidden ownership

A field named items does not reveal whether the owner creates, shares, or merely observes those items. Document and enforce lifecycle rules.

Mutable exposure

Returning an internal list allows clients to bypass invariants. Return an immutable view, a copy, or an iterator where appropriate. Java provides unmodifiable collection views; Python designs commonly return tuples or copies.

Equality and identity

Two objects may represent the same domain entity even if they are different memory objects. Define equality deliberately, especially when objects are placed in sets or used as dictionary keys. Identity is about whether two references point to the same object; equality is about whether their values or domain identity are considered equivalent.

Persistence and serialization

Object relationships must often be mapped to databases or APIs. Composition may map naturally to dependent rows, while associations may use foreign keys or join tables. Serialization of bidirectional graphs can cause cycles, so APIs should define boundaries and use identifiers or carefully designed data-transfer objects.

Security and privacy

Relationships can expose sensitive data. A User object should not automatically expose every PaymentMethod or medical record associated with it. Apply least privilege, explicit authorization, and controlled accessors. Do not confuse an in-memory reference with permission to view or modify data.

11. Contemporary Development Practices

Modern applications frequently use dependency injection frameworks, interface-based design, immutable value objects, and automated tests. Java applications may use interfaces, records, sealed classes, and modules to make allowable relationships clearer. Python applications commonly use protocols, dataclasses, type hints, and composition through constructor injection.

AI-assisted coding tools can suggest inheritance structures or generate boilerplate, but they cannot reliably determine domain ownership or substitutability without contextual requirements. Developers must review generated designs for lifecycle correctness, security, testability, and compliance with business rules. A generated subclass that compiles may still violate the base class contract.

A practical review checklist is:

  1. Is every inheritance edge a defensible “is-a” relationship?
  2. Can each subtype satisfy the base contract?
  3. Should the relationship instead be a field, parameter, or strategy?
  4. Who creates and destroys each part?
  5. Can either object exist independently?
  6. Are multiplicities and invariants enforced?
  7. Are mutable collections protected?
  8. Can collaborators be replaced in tests?
  9. Does the design expose more data or authority than necessary?
  10. Will likely future changes require new subclasses or merely new components?

Chapter Summary

Object relationships express how software objects know, use, contain, and specialize one another. Association connects independently meaningful objects. Dependency represents temporary use. Aggregation is a weak whole–part relationship in which parts can exist independently, whereas composition gives the whole stronger ownership of part lifecycle. Inheritance expresses an “is-a” relationship and should be used only when the subtype is safely substitutable for its base type.

Method overriding enables specialized behavior and runtime polymorphism, but an override must preserve the conceptual contract of the inherited operation. Composition expresses “has-a” relationships and often produces more flexible, testable, and replaceable designs. Dependency injection and strategy objects are practical applications of this principle. Effective design also requires attention to multiplicity, encapsulation, equality, persistence, security, and lifecycle semantics.

Key Terms

Association: A structural connection between objects or classes.

Dependency: A temporary use relationship between a client and a supplier.

Aggregation: A weak whole–part relationship in which parts can exist independently.

Composition: A strong whole–part relationship in which the whole controls part ownership or lifecycle.

Inheritance: A mechanism by which a class derives from another class.

Is-a relationship: A specialization relationship supporting substitutability.

Has-a relationship: A containment, collaboration, or assembly relationship.

Multiplicity: The allowed number of related objects.

Polymorphism: The ability to use one interface for objects with different implementations.

Method overriding: Replacing or specializing inherited behavior with a compatible method.

Substitutability: The ability to use a subtype where its base type is expected.

Liskov Substitution Principle: The principle that subtypes must preserve the correctness expectations of their base types.

Dependency injection: Supplying a dependency from outside an object rather than constructing it internally.

Delegation: Passing responsibility for an operation to another object.

Coupling: The degree of dependence between software elements.

Cohesion: The degree to which the responsibilities of one element belong together.

Invariant: A condition that should remain true for an object or system.

Interface: A contract specifying operations a class can provide.

Lifecycle: The creation, use, persistence, transfer, and destruction of an object.

Review Questions

Short Answer Questions

  1. What distinguishes association from dependency?
  2. Explain the difference between aggregation and composition.
  3. What is meant by an “is-a” relationship?
  4. Why is method overriding important for polymorphism?
  5. State two reasons composition may be preferred over inheritance.

Long Answer Questions

  1. Compare association, aggregation, composition, and dependency using a university learning platform as an example.
  2. Explain the Liskov Substitution Principle and discuss how an incorrect override can violate it.
  3. Describe how dependency injection improves testability and reduces coupling.
  4. Discuss the design risks of deep inheritance hierarchies.
  5. Explain how multiplicity, ownership, and lifecycle rules influence object design.

Analytical / Application Questions

  1. A Library creates BookCopy objects, while Author objects may be shared by many books. Identify the likely relationships and justify your choices.
  2. A developer creates subclasses EmailReport, PDFReport, and CSVReport, although the only difference is output formatting. Redesign the model using composition and explain the benefit.
  3. Evaluate whether Penguin should inherit from a Bird class containing a fly() method. Propose an alternative design if the inheritance contract is inappropriate.

Multiple Choice Questions with Answers

Question 1

Which relationship most strongly indicates that the whole controls the lifecycle of its parts?

A. Dependency

B. Association

C. Aggregation

D. Composition

Correct Answer: D. Composition

Question 2

A class receives a formatter as a method parameter and uses it only during that method call. This is primarily a:

A. Dependency

B. Composition

C. Inheritance relationship

D. Generalization hierarchy

Correct Answer: A. Dependency

Question 3

Which statement best describes an “is-a” relationship?

A. An object temporarily calls another object

B. A subtype can validly be used where its base type is expected

C. A whole owns a collection of independent parts

D. Two objects share a database table

Correct Answer: B. A subtype can validly be used where its base type is expected

Question 4

What does method overriding provide in an object-oriented program?

A. A way to prevent all inheritance

B. A way to replace or specialize inherited behavior

C. A way to make every method static

D. A way to eliminate object identity

Correct Answer: B. A way to replace or specialize inherited behavior

Question 5

Why is constructor-based dependency injection useful?

A. It hides all dependencies from clients

B. It prevents objects from having behavior

C. It makes required collaborators explicit and replaceable

D. It forces every class into an inheritance hierarchy

Correct Answer: C. It makes required collaborators explicit and replaceable

Question 6

Which example most naturally represents composition?

A. A doctor and a patient

B. A report printer and a formatter passed to a method

C. An order and the order lines created for that order

D. A student and a university course

Correct Answer: C. An order and the order lines created for that order

Question 7

A class inherits from another class only to reuse two unrelated utility methods. What is usually the better design recommendation?

A. Create a deeper inheritance hierarchy

B. Use composition or extract the shared behavior

C. Make all methods private

D. Duplicate every method without analysis

Correct Answer: B. Use composition or extract the shared behavior

Question 8

What is the main purpose of the Liskov Substitution Principle?

A. To require multiple inheritance

B. To ensure subtypes preserve expectations of their base types

C. To prevent method overriding

D. To require every class to own its dependencies

Correct Answer: B. To ensure subtypes preserve expectations of their base types

Question 9

In a learning platform, a student can enroll in several courses, and courses can continue to exist after a student withdraws. This is best modeled as:

A. Composition between student and course

B. Inheritance between student and course

C. Association between student and course

D. Dependency in which the course owns the student

Correct Answer: C. Association between student and course

Question 10

Which design most directly supports replacing a tax calculation policy without creating many invoice subclasses?

A. Composition with a tax-policy collaborator

B. Private inheritance from every tax class

C. A fixed conditional embedded in every invoice method

D. Making the invoice class a subclass of a tax policy

Correct Answer: A. Composition with a tax-policy collaborator

Object-Oriented Programming (Java/Python)
Chapter 5 of 15