Projects: Banking System & School Management System

Acadestine

Learning Objectives
    • Differentiate between 'is-a' (inheritance) and 'has-a' (composition/aggregation) class relationships.
    • Design object architectures where classes contain instances of other classes as attributes.
    • Distinguish between strong lifecycle coupling (composition) and independent lifecycle references (aggregation).

Building Complex Systems with Modular Blocks

When you first start writing Python, it is common to put all your code into a single script to complete a specific task. But real-world software like online banking platforms or university management systems is built by connecting many independent, modular blocks together.

As your projects expand, putting all your logic into one place creates a fragile codebase that is difficult to maintain or debug. To manage complexity in larger projects, engineers rely on object-oriented architecture to divide big systems into smaller, focused components.

Consider how these software structures differ:

  • Single-file scripts: Great for quick automation tasks, but they become overwhelming and hard to update as features grow.
  • Modular architectures: Essential for production applications because they separate responsibilities into distinct parts.
  • Interconnected objects: Allow individual pieces such as a Customer and an Account to work together while keeping their internal logic clean and self-contained.

Understanding how to structure these relationships is the critical step that transforms you from someone who writes standalone scripts into an engineer who builds scalable, enterprise-ready software.

Why Inheritance Isn't Always the Answer

When you first learn Object-Oriented Programming, inheritance feels like a superpower that solves every design problem by sharing code between parent and child classes. However, forcing every relationship into a rigid inheritance tree creates brittle codebases that break the moment your real-world requirements change.

Think of it like building a smartphone. If you designed a phone by forcing it to inherit directly from a standalone camera, a landline telephone, and a handheld video game console, you would end up with a tangled mess. If the landline class changes its wire voltage requirement, your smartphone suddenly crashes!

Let me show you how this problem manifests in actual Python code when inheritance trees grow too deep.

class Vehicle:
    def start(self):
        print("Vehicle power turned on.")

class GasCar(Vehicle):
    def fill_tank(self):
        print("Filling tank with gasoline...")

class ElectricCar(GasCar):
    # ElectricCar inherits from GasCar to reuse start() and driving logic
    pass

# Creating an instance of ElectricCar
my_tesla = ElectricCar()
my_tesla.start()
my_tesla.fill_tank()
Vehicle power turned on.
Filling tank with gasoline...

Breaking Down the Inheritance Trap

Let's look at what went wrong in the example above:

  1. Line 5 (class GasCar(Vehicle):): We defined GasCar as a subclass of Vehicle and added the fill_tank() method.
  2. Line 9 (class ElectricCar(GasCar):): To reuse existing vehicle behavior, ElectricCar inherits directly from GasCar.
  3. Line 15 (my_tesla.fill_tank()): Because ElectricCar is at the bottom of a deep inheritance tree, it forced our electric car to inherit behavior it shouldn't have, claiming to fill an electric vehicle with gasoline!

This issue is known as tight coupling. When classes are tightly coupled through deep inheritance chains, a change to a parent class higher up the chain automatically ripples down to every child class below it, whether those children want that behavior or not.

The Power of Loose Coupling and Modular Design

To fix this problem, experienced developers favor loose coupling and modular design. Instead of forcing classes into rigid "is-a" hierarchies, you build your software using smaller, independent components that can be plugged together as needed.

By shifting away from deep inheritance hierarchies toward modular components, you unlock several key advantages:

  • Flexibility: You can easily swap or update individual modules without breaking unrelated classes.
  • Maintainability: Isolated components mean bug fixes in one area won't cause unexpected side effects across your entire codebase.
  • Reusability: Small, independent classes can be reused in completely different parts of your application without bringing along unwanted parent dependencies.

In the upcoming sections, you will learn how to connect these modular blocks together so your classes remain clean, focused, and adaptable.

The 'Has-A' Relationship: Cars and Engines

Think about how complex objects are built in the real world you rarely construct a complicated machine from a single solid block of material. Instead, you build complex systems by assembling smaller, specialized components into a larger container object.

To design clean object architectures, you need a simple mental test to decide how two concepts relate to each other: the 'Is-A' rule versus the 'Has-A' rule.

The Speech Test: 'Is-A' vs. 'Has-A'

When you are trying to figure out how two objects should interact, say their relationship out loud:

  • The 'Is-A' Test (Inheritance): Ask yourself, "Is Object A a specialized version of Object B?" For instance, a SportsCar is a Car. This represents inheritance, where the child object inherits attributes and behaviors directly from the parent.
  • The 'Has-A' Test (Containment): Ask yourself, "Does Object A contain Object B as a part?" For instance, a Car has an Engine. A car is not an engine itself; rather, the car acts as a container object that holds the engine as a nested component object.

If you try to use inheritance where containment belongs, your design quickly breaks down. Saying "a Car is an Engine" makes no sense in the real world, and forcing that relationship in your code creates rigid, confusing software.

Breakdown of the Metaphor

Here is how real-world assembly maps directly to object-oriented architectural concepts:

Relationship Type Real-World Metaphor Architectural Meaning Example
'Is-A' Categorization / Specialization An object is a specific variant of a broader category. A Sedan is a Car.
'Has-A' Assembly / Containment A container object holds one or more nested component objects to delegate work. A Car has an Engine.

By treating your main objects as containers and giving them smaller nested components, you create modular designs where individual parts can be swapped, repaired, or upgraded without tearing down the entire structure.

Composition and Aggregation Mechanics

Understanding how classes interact is the hallmark of clean object-oriented architecture. By storing class instances as attributes within other classes, you can build complex systems out of simple, reusable building blocks.

In Python, these design patterns fall into two categories: composition (strong ownership) and aggregation (weak ownership). The core difference comes down to object lifecycles specifically, whether a child object can exist without its parent object.

Feature Composition (Strong Ownership) Aggregation (Weak Ownership)
Lifecycle Coupling Tightly bound. If the parent is destroyed, the child is destroyed too. Loosely bound. The child exists independently of the parent.
Instantiation The child is created inside the parent class. The child is created outside and passed into the parent.
Relationship Example A Car and its Engine. A Department and its Employee objects.

Code Implementation

Run this code in your environment to see how composition and aggregation manage their nested objects differently:

Console

        

Output

Vroom! Engine with 300 HP is running.
Engineering Department members:
- Alice
- Bob

A common beginner mistake is thinking composition requires special Python keywords. In reality, composition is just instantiating a class inside another class's __init__() method, while aggregation is passing an existing instance in as a parameter!

The Breakdown

Let me walk you through how storing instances as attributes works in both approaches:

  • Composition Mechanics (Car and Engine):
  • Inside Car.__init__(), we write self.engine = Engine(horsepower).
  • The Car class is directly responsible for instantiating Engine.
  • You access the nested object's methods using dot notation: my_car.engine.start().
  • If my_car is deleted or garbage-collected, its internal self.engine reference dies with it.

  • Aggregation Mechanics (Department and Employee):

  • Notice that emp1 and emp2 are created on their own lines of code, completely outside Department.
  • We pass these existing Employee objects into tech_dept.add_employee(emp1).
  • The Department stores these references inside its self.team list attribute.
  • If tech_dept is deleted, emp1 and emp2 still exist in memory because they were created independently.

Visualizing Bank and School System Architectures

When building multi-class enterprise projects, visualizing how objects connect and pass messages down the line is essential for clean architecture. Mapping out how top-level classes delegate responsibilities to nested objects prevents your code from devolving into a tangled web.

To build scalable systems, you must understand both ownership chains and communication pathways. Let's look at how two distinct multi-class chains organize their relationships and behavior:

Architecture Chain Class Scope Relationship Type Communication Pathway
Bank $\rightarrow$ Account $\rightarrow$ Transaction Financial System Composition (Transaction history is tightly coupled to Account) Bank delegates financial requests to Account, which instantiates and logs a Transaction.
School $\rightarrow$ Classroom $\rightarrow$ Student Educational System Aggregation (Student exists independently of Classroom) School routes administrative queries to Classroom, which inspects its list of Student objects.

In both architectures, top-level manager classes (Bank and School) should not manipulate lower-level data directly. Instead, they use object delegation, where higher-level classes delegate work down to the appropriate nested object.

Run this complete Python script to see how messages flow through a multi-tiered Bank $\rightarrow$ Account $\rightarrow$ Transaction chain:

Console

        

Expected Output

--- Initializing Bank System ---
Statement for Account: ACC-9876
  - [DEPOSIT] $500.00
  - [DEPOSIT] $150.50
Current Balance: $650.50

Code Breakdown

  • Line 1–8: The Transaction class represents the leaf node of our chain. It holds basic financial data and exposes get_summary() to handle its own display logic.
  • Line 11–27: The Account class acts as the middle tier. It stores Transaction instances in self.transactions. When add_transaction() is called, it creates a Transaction object internally, demonstrating strong lifecycle ownership (composition).
  • Line 30–46: The Bank class acts as the top-level interface. Its self.accounts dictionary tracks Account instances.
  • Line 38–41: Inside process_deposit(), notice how Bank does not modify balance or list transactions directly. Bank delegates the task down to target_account.add_transaction(), keeping object boundaries clean.
  • Line 24–25: Similarly, Account delegates string formatting down to tx.get_summary(). Communication travels sequentially through each layer of the hierarchy (Bank $\rightarrow$ Account $\rightarrow$ Transaction).

Architectural Design Blueprint Summary

Before you jump into writing larger multi-class Python applications, you need a quick mental blueprint to pick the right relationship between your objects. Choosing the correct class relationship upfront prevents rigid code and saves you from massive refactoring headaches later.

Here is a quick recap of the core criteria and lifecycles you should keep in mind as you design your system architecture.

The Decision Blueprint

When deciding how two classes should interact, ask yourself two simple questions:

  • Is class B a specialized type of class A? If yes, you are looking at an Is-A relationship (Inheritance). Use this sparingly to share common attributes and behavior.
  • Does class A contain or use class B? If yes, you are looking at a Has-A relationship. You will attach an instance of class B as an attribute inside class A.

If your answer points to a Has-A relationship, you must then check the lifecycle coupling:

  • Composition (Strong Lifecycle): The child object cannot exist without the parent object. Creating or destroying the parent directly creates or destroys the child inside its __init__() method.
  • Aggregation (Weak Lifecycle): The child object exists independently. It is created outside the parent class and passed in (usually as an argument), meaning it survives even if the parent object is destroyed.

Relationship Comparison Cheat Sheet

Use this reference table when planning your object interactions:

Relationship Category Lifecycle Dependency Real-World Metaphor
Inheritance Is-A High (Class-level coupling) A SavingsAccount is a BankAccount
Composition Has-A High (Child dies with Parent) A House has a Room
Aggregation Has-A Low (Child lives independently) A Classroom has a Student

Always default to composition or aggregation over inheritance whenever possible. This keeps your code modular, easier to test, and flexible enough to adapt as your project grows.

Previous Next Lesson