Decoding Relationships Between Classes: A Comprehensive Exploration

Establishing relationships between classes is fundamental in object-oriented programming. Without having relationships among objects, we can’t able to model them as real-world entities. In this post, we will explore relationships between classes in detail and understand their differences, use cases, and benefits.

Relationships Between Classes

Common relationships

  • Dependency (Uses-a)
  • Inheritance (Is-a)
  • Association (Has-a)
Class Relationships

├── 1. Dependency ("Uses-a")
│      └── Weakest, transient relationship (e.g., local method parameter or return type)

├── 2. Generalization / Realization ("Is-a")
│      ├── Inheritance (Class extends Class)
│      └── Interface Realization (Class implements Interface)

└── 3. Association ("Has-a" / Structural link via instance attributes)

       ├── A. By Navigability
       │      ├── Unidirectional (A ──> B)
       │      └── Bidirectional  (A ──── B)

       ├── B. By Cardinality / Multiplicity
       │      ├── One-to-One   (1 : 1)
       │      ├── One-to-Many  (1 : *)
       │      └── Many-to-Many (* : *)

       ├── C. By Association Constructs
       │      ├── Reflexive / Recursive (Class references its own type)
       │      ├── Association Class (The link itself has attributes/methods)
       │      └── Qualified Association (Keyed lookup/map indexing)

       └── D. By Whole-Part (Aggregation Spectrum)
              ├── Aggregation (Weak Whole-Part, independent lifecycles, shared ownership)
              └── Composition (Strong Whole-Part, bound lifecycles, exclusive ownership)
Python

Dependency

A Dependency in Object-Oriented Design is a “uses-a” relationship where one class relies on another class temporarily to carry out an operation, but does not store or maintain a reference to it as an instance attribute.

How Dependency Appears in Code

A dependency exists when Class A interacts with Class B in one of three transient ways:

  • Method Parameter: Class B is passed into a single method of Class A.
  • Local Variable: Class A instantiates or uses Class B purely inside a local method scope.
  • Return Type: A method in Class A returns an instance of Class B.

Dependency via Method Parameter (Passed In)

The client class receives the supplier object only for the duration of a single method execution.

class Printer:
    def print_file(self, content: str):
        print(f"Printing: {content}")

class Document:
    def __init__(self, text: str):
        self.text = text

    # DEPENDENCY: Printer is only used inside this method.
    # Document does NOT keep a self.printer attribute.
    def export(self, printer: Printer):
        printer.print_file(self.text)
Python

Dependency via Local Instantiation (Created Locally)

The client class creates an instance of the supplier inside a method and discards it as soon as the method exits.

class JsonFormatter:
    def format(self, data: dict) -> str:
        import json
        return json.dumps(data)

class ReportGenerator:
    def generate_summary(self, raw_data: dict) -> str:
        # DEPENDENCY: Formatter is instantiated locally and destroyed after execution
        formatter = JsonFormatter()
        return formatter.format(raw_data)
Python

Dependency via Return Type (Factory Method)

A factory method depends on the class it constructs and returns.

class PDFReport:
    def __init__(self, title: str):
        self.title = title

class ReportFactory:
    # DEPENDENCY: ReportFactory depends on PDFReport as its return type
    def create_pdf(self, title: str) -> PDFReport:
        return PDFReport(title=title)
Python

Inheritance

  • Inheritance represents an “IS-A” relationship
  • It enables a class to inherit properties and behaviours from another class.
  • Inheritance establishes a relationship between a base class (also known as a superclass, parent class) and a derived class (also known as a subclass, child class).
  • The derived class can reuse the attributes and methods of the base class, allowing for code reuse and the creation of specialised classes.

Example of Inheritance

  • Dog “IS-A” type of Animal: Dog inherits a few properties and behaviours from Animal, plus it has their own properties and behaviours.
  • Car “IS-A” type of vehicle
  • Rose “IS-A” type of flower

Python Code Example :

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"
Python

In this example, both Dog and Cat classes inherit the speak() method from the Animal class.

Benefits of Inheritance:

  • Code Reusability: Inherited properties and methods can be reused in the derived class, reducing redundancy and promoting efficient code development.
  • Polymorphism: Inheritance allows for polymorphism, where objects of different classes can be treated as objects of the same base class, enabling dynamic method binding and flexibility in the code.
  • Easy Maintenance: Changes made to the base class are automatically reflected in the derived classes, simplifying maintenance and updates.

Association

  • The association represents a “has-a” relationship, where one class has an instance of another class.
  • We can say one class stores the objects of another class as its instance variable.

Benefits of Association:

  • Modularity: Classes can be developed independently, enhancing modularity and maintainability of the code.
  • Flexibility: Changes in one class do not necessarily affect other classes, promoting flexibility and scalability.
  • Code Organisation: Classes can be organised and structured based on their relationships, improving code readability and understanding.

Forms of association

  • Composition
  • Aggregation
association

An association can be viewed as the most general concept, where objects have some kind of relationship.

Aggregation is a type of association where there is a whole-part relationship, but the parts are independent.

Composition is the strongest type of association, where the parts are entirely dependent on the whole.

The distinction between composition and aggregation lies in the strength of the relationship and the independence of the lifecycle between the classes involved.

Whole-part relationship

A whole-part relationship in Object-Oriented Design represents a “has-a” relationship where one class (the whole) is composed of or contains one or more instances of other classes (the parts).

Identification of whole and part

Example 1: Car has engine or Engine has Car

“Car has engine” is more accurate.

So, Car is the whole, while engine is part

Example 2: Library has book or book has Library 

“Library has book” is more accurate

So, Library is the whole, while book is part 

Composition

  • The composition represents a stronger form of association.
  • It represents a whole-part relationship where the part cannot exist independently of the whole.
  • Strong relationship: The part’s lifecycle is strictly tied to the whole’s lifecycle. If the whole object is destroyed, the part is also destroyed. They cannot exist independently.

Let’s clarify how composition can be considered both a “has-a” relationship and a “part-of” relationship:

class Car:
  def __init__(self):
      self.engine = Engine() # Composition
      self.wheels = Wheels() # Composition
      self.seats  = Seats() # Composition
Python

Has-A Relationship:

  • Composition represents a “has-a” relationship because a class can contain objects of other classes as its members or attributes. For example, a car class can have attributes like an engine, wheels, and seats. Each of these attributes is an object of another class, and the car “has” these components.
  • In this example, the `Car` class has attributes that are objects of other classes, indicating a “has-a” relationship through composition.

Part-of Relationship:

  • Composition also represents a “part-of” relationship because the objects of one class are essential parts of another class. The components are intimately related to the whole and are considered integral parts of the containing class.
  • `Engine`, `Wheels`, and `Seats` classes belong to the `Car` class.
  • `Engine`, `Wheels`, and `Seats` classes are considered parts of the `Car` class.
  • If the `Car` object is destroyed, its components are typically destroyed as well, indicating a “part-of” relationship through composition.

So, the composition can be described as both a “has-a” relationship (because one class has objects of another class as its members) and a “part-of” relationship (because the objects of one class are integral parts of another class).

Example of Composition

  • A car consists of an engine. The engine and the car are tightly bound; the car cannot function without the engine.
  • A house is composed of various rooms such as bedrooms, living rooms, kitchens, etc. Each room is an integral part of the house, and the house is a composite object of these rooms. If the house is demolished, the rooms cease to exist.

Aggregation

  • Aggregation is a weak association
  • It is a loosely coupled relationship compared to composition
  • The aggregated class is not entirely dependent on the containing class.
  • Weak relationship: The lifecycle of the part is independent of the whole. If the whole object is destroyed, the part can still exist.

Example of Aggregation in Python:

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

class University:
    def __init__(self, name, department):
        self.name = name
        self.department = department

math_department = Department("Mathematics")
my_university = University("Example University", math_department)

print(my_university.department.name)  # Output: Mathematics
Python

In this example, the University class has an aggregation of the Department class, allowing it to represent the relationship between a university and its departments. University and department can exist independently

Advantages of object composition and aggregation over inheritance

  • Inheritance breaks encapsulation: By inheriting from a class, you’re coupling the child class with several potential implementation details of the parent.
  • Composition is more flexible than inheritance: You can change the implementation of a class at run-time by changing the included object, thus changing its behaviour, but you can’t do this with inheritance; you can’t change the behaviour of the base class at run-time.
  • It is possible to implement “multiple inheritance” in languages that do not support it by composing multiple objects into one.
  • There is no conflict between methods/properties names, which might occur with inheritance.

The downside of composition and aggregation is:

  • The system’s behaviour may be harder to understand just by looking at the source code since it’s more dynamic and more interaction between classes happens at runtime, rather than compile time.

UML class diagrams

uml class diagram relationships

Aggregation

association

We are considering a car and a wheel example. A car cannot move without a wheel. But the wheel can be independently used with a bike, scooter, cycle, or any other vehicle. The wheel object can exist without the car object, which proves to be an aggregation relationship.

Composition

The composition association relationship connects the Person class with the Brain class, Heart class, and Legs class. If the person is destroyed, the brain, heart, and legs will also be discarded.

composition

The order of strength from strong to weak is:

inheritance → implementation → composition → aggregation → association → dependency 
Python

Generalization:

Generalisation is a synonym for inheritance in the world of OOP. When a class is inherited from another class, then we can show this inheritance relationship with a simple arrow from the child class to the parent class.

Toyota Corolla and Ford Explorer are both popular cars in 2021. Since they are specific to Cars, they can inherit from a generalisation of Cars. Thus, they can be children of the general class Car.

Realization

Realisation is also a type of inheritance but for interfaces. In a realisation relationship, one entity (normally an interface) defines a set of functionalities as a contract, and the other entity (normally a class) realises the contract by implementing the functionality defined in the contract.

Dependency:

One class is dependent on another class.

Inheritance vs Association

Inheritance

  • Inheritance is a mechanism where a new class (derived or child class) inherits the properties and behaviours (attributes and methods) of an existing class (base or parent class).
  • It represents an “is-a” relationship.
  • Purpose: Inheritance promotes code reusability by allowing a child class to reuse methods and attributes of the parent class. The child class can also override or extend the functionality of the parent class.
  • Example:
    • The dog is an Animal
    • A student is a Person

Association

  • The association represents a relationship where two or more classes are connected but remain independent of each other.
  • It represents a “has-a” relationship and is used to show how objects interact with each other.
  • Purpose: Association describes how objects work together, but each object has its own lifecycle and can exist independently of the other.
  • Example
    • The book has a Page
    • The car has an engine

Inheritance

class Animal:
    def speak(self):
         ..........

class Dog(<strong>Animal</strong>):
    def speak(self):
         ..........
Python

Association

class Teacher:
    def some_function(self, student):
         ..........

class Student:
    def learn(self, teacher):
       Teacher().some_function()
Python

Conclusion: Choosing the Right Approach

In summary, inheritance, composition, and aggregation are powerful tools in the world of object-oriented programming. Each concept offers unique benefits and use cases, allowing developers to design sophisticated systems while maintaining code readability, reusability, and flexibility. By understanding these concepts and choosing the right approach based on the specific requirements of a project, developers can create robust and maintainable software solutions.

As a developer, mastering these concepts will empower you to design elegant and efficient object-oriented systems, ensuring your codebase remains scalable and adaptable to future changes. Happy coding!

Resources

For further exploration, make sure to check out these helpful resources:


About Puneet Verma

Puneet Verma is a software developer specialising in backend architecture, Dynamic Programming, and SaaS solutions. He focuses on building optimised, scalable applications and sharing deep-dive technical tutorials to help developers master complex algorithmic patterns.

1 thought on “Decoding Relationships Between Classes: A Comprehensive Exploration”

Leave a Comment