Programming in C++/Java/Python

Chapter 7
Object-Oriented Programming

Diploma in Software Engineering
Programming in C++/Java/Python   •   Diploma

Object-Oriented Programming

Introduction and Context

Object-oriented programming (OOP) is a way of designing software around objects that combine data with the operations that use that data. Instead of treating a program only as a sequence of procedures, OOP models a system as a set of cooperating entities. A bank account, product, student, sensor, or file may be represented as an object with state and behaviour.

OOP is especially useful when software is large, evolves over time, and must be maintained by teams. It supports separation of responsibilities, reuse, controlled access to data, and substitution of related implementations. C++, Java, and Python all support object-oriented programming, although they express some ideas differently. C++ provides classes, deterministic resource management, multiple inheritance, and templates. Java provides class-based object orientation, garbage collection, and interfaces. Python supports classes and objects with dynamic typing, duck typing, and multiple inheritance.

A good object-oriented design is not merely a collection of classes. It identifies meaningful responsibilities, defines stable interfaces, limits unnecessary dependencies, and uses inheritance only where a genuine substitutable relationship exists.

Learning Objectives

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

  • explain the relationship between classes, objects, state, and behaviour;
  • define and use constructors, methods, attributes, and access control;
  • apply encapsulation and distinguish it from simple information hiding;
  • model relationships using inheritance and composition;
  • explain compile-time and run-time polymorphism;
  • design and use abstract classes and interfaces;
  • compare relevant C++, Java, and Python idioms;
  • select an appropriate object-oriented design for a practical problem;
  • identify common design errors, ethical risks, and maintenance limitations.

1. Fundamental Concepts

1.1 Classes and objects

A class is a definition or blueprint. It describes the data and operations that instances of the class may have. An object is a concrete instance created from that definition.

For example, a BankAccount class may define an account number, balance, deposit operation, and withdrawal operation. Two objects created from the class represent two different accounts and normally have different state values.

The main elements are:

  • Attribute or field: data belonging to an object, such as balance.
  • Method: an operation that belongs to a class, such as deposit().
  • State: the values of an object's attributes at a particular time.
  • Behaviour: the operations an object can perform.
  • Identity: the property that distinguishes one object from another, even when their state is equal.
  • Message or method call: a request for an object to perform an operation.

A class should represent a meaningful abstraction rather than an arbitrary collection of unrelated variables and functions.

1.2 A simple class in three languages

The following examples model a temperature reading. They deliberately expose only a small, coherent interface.

C++:

class Temperature {
private:
    double celsius;

public:
    explicit Temperature(double value) : celsius(value) {}

    double value() const {
        return celsius;
    }

    void increase(double degrees) {
        celsius += degrees;
    }
};

Java:

public class Temperature {
    private double celsius;

    public Temperature(double value) {
        this.celsius = value;
    }

    public double value() {
        return celsius;
    }

    public void increase(double degrees) {
        celsius += degrees;
    }
}

Python:

class Temperature:
    def __init__(self, value):
        self._celsius = value

    @property
    def value(self):
        return self._celsius

    def increase(self, degrees):
        self._celsius += degrees

The three implementations have the same conceptual model. Their syntax and enforcement mechanisms differ. C++ and Java can enforce private access at compile time. Python uses conventions such as a leading underscore and provides mechanisms such as properties, but it generally emphasises programmer discipline rather than strict access restriction.

2. Constructors and Object Lifetime

A constructor establishes a valid initial state for an object. It is called when an object is created. Constructors should initialise all required fields and reject invalid input where appropriate.

A class may provide more than one constructor, provided the parameter lists differ. This is called constructor overloading. A default constructor accepts no arguments, while a parameterised constructor requires initial values.

C++ also has a destructor, which is called when an object’s lifetime ends. Destructors are important for releasing resources such as files, locks, or memory. Modern C++ commonly uses Resource Acquisition Is Initialization (RAII): an object acquires a resource during construction and releases it automatically during destruction.

Java objects are reclaimed by garbage collection, but external resources still require explicit management. The try-with-resources statement is preferred for objects implementing AutoCloseable.

Python uses reference counting in its standard implementation together with cyclic garbage collection, but programmers should not rely on object destruction for timely release of files or network connections. The with statement is preferred for managed resources.

Example in Java:

try (var reader = new java.io.BufferedReader(
        new java.io.FileReader("data.txt"))) {
    String line = reader.readLine();
}

The resource is closed when the block ends, including when an exception occurs.

Constructor design principles

A constructor should:

  1. establish class invariants;
  2. validate values that must satisfy a rule;
  3. avoid performing surprising external actions;
  4. avoid calling overridable methods during construction;
  5. keep object creation understandable and testable.

A class invariant is a condition that should remain true for every valid object. For a bank account, a possible invariant is that an account number is non-empty and a withdrawal cannot leave the balance below an authorised limit.

3. Encapsulation and Information Hiding

Encapsulation means bundling state and operations into a unit and controlling how that unit is used. Information hiding means concealing design decisions that other parts of the program do not need to know.

These ideas are related but not identical. A class can encapsulate data and methods while still exposing too many implementation details. Good design exposes a small, meaningful interface and hides changeable implementation choices.

Access modifiers commonly include:

ModifierC++JavaPython convention
PublicAccessible wherever visibleAccessible to permitted clientsOrdinary public attribute or method
PrivateAccessible only to class and permitted friendsAccessible only within the class_name convention; name mangling for __name
ProtectedClass and derived classesClass, package, and derived access rulesConvention rather than strict enforcement

Consider a bank account. If clients can directly assign balance, they can bypass validation. A better design offers operations such as deposit() and withdraw() that protect the invariant.

class BankAccount:
    def __init__(self, account_number, opening_balance=0.0):
        if not account_number:
            raise ValueError("account number is required")
        if opening_balance < 0:
            raise ValueError("opening balance cannot be negative")
        self._account_number = account_number
        self._balance = opening_balance

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0 or amount > self._balance:
            raise ValueError("invalid withdrawal")
        self._balance -= amount

The balance property provides read access without allowing arbitrary assignment. This is preferable to exposing a public mutable field.

Encapsulation does not require that every field have a getter and setter. Automatically generating getters and setters for all fields may merely reproduce public data access in a more verbose form. A method should represent a meaningful operation or policy.

4. Relationships Between Classes

Object-oriented models commonly contain three relationships.

4.1 Association

An association means that objects know about or interact with one another. A Teacher may teach a Course, and a Customer may place an Order. Neither object necessarily owns the lifetime of the other.

4.2 Aggregation

Aggregation is a whole–part relationship in which the parts can exist independently. A Department may contain Professor objects, but professors may continue to exist if the department is reorganised.

4.3 Composition

Composition is a stronger whole–part relationship. The whole controls the lifetime or meaningful existence of the parts. An Order may contain OrderLine objects created specifically for that order. If the order is deleted, its lines normally have no independent purpose.

Composition is often implemented by storing one object inside another or by storing references to owned objects. In C++, ownership may be expressed using std::unique_ptr; in Java and Python, object references are managed by the runtime, but design ownership still needs to be made clear.

4.4 Prefer composition when appropriate

The principle “prefer composition over inheritance” does not mean inheritance is forbidden. It means that assembling objects with focused responsibilities is often more flexible than creating a deep hierarchy.

For example, a payment service might contain a PaymentProcessor object. Replacing the processor with a test double or a different provider is easier than creating many subclasses of PaymentService.

Composition reduces coupling because the containing class depends on a capability or interface rather than on the internal details of a superclass.

5. Inheritance

Inheritance allows a new class, called a subclass or derived class, to reuse and extend an existing superclass or base class.

A valid inheritance relationship should satisfy an “is-a” test and the Liskov Substitution Principle: an object of the subtype should be usable wherever an object of the supertype is expected without breaking the program’s required behaviour.

For example, a SavingsAccount may be a kind of BankAccount if it obeys the behavioural expectations of a bank account. A Square is mathematically a rectangle, but modelling Square as a subtype of a mutable Rectangle can violate expectations if width and height are changed independently. This illustrates why linguistic “is-a” relationships are not sufficient; behaviour matters.

Inheritance can provide:

  • reuse of common implementation;
  • a shared interface;
  • polymorphic substitution;
  • a natural classification where the hierarchy is stable.

It can also introduce tight coupling, fragile base-class problems, complicated constructors, and unwanted inherited operations. Public inheritance in C++ and class inheritance in Java should therefore be intentional. Python inheritance should likewise be based on a clear behavioural contract.

6. Polymorphism

Polymorphism means that one interface can represent or invoke different underlying forms. It is a central benefit of object-oriented design.

6.1 Compile-time polymorphism

Compile-time polymorphism is resolved before execution. Examples include function overloading, operator overloading in C++, and generic methods. The compiler selects an appropriate operation based on argument types and context.

int maximum(int a, int b);
double maximum(double a, double b);

The two functions have the same name but different parameter types.

6.2 Run-time polymorphism

Run-time polymorphism selects an overridden operation according to the actual object associated with a reference. It is commonly implemented using virtual methods in C++ and overridden methods through superclass references in Java. Python usually achieves the same effect through dynamic dispatch and duck typing.

C++:

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
private:
    double radius;
public:
    explicit Circle(double r) : radius(r) {}
    double area() const override {
        return 3.141592653589793 * radius * radius;
    }
};

The virtual destructor is important when an object may be deleted through a base-class pointer. The override keyword asks the compiler to verify that a method actually overrides a base method.

Java:

abstract class Shape {
    public abstract double area();
}

class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

Python:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.141592653589793 * self.radius ** 2

def total_area(shapes):
    return sum(shape.area() for shape in shapes)

In the Python example, an object need not inherit from a particular base class. It needs to provide the expected area() operation. This is called duck typing: suitability is determined by supported behaviour rather than declared class membership.

6.3 Method overriding and overloading

Overriding occurs when a subclass supplies a new implementation of an inherited method with the same conceptual signature. Overloading uses the same method name with different parameter lists. Java and C++ support conventional overloading. Python does not support signature-based method overloading in the same way; a later method definition replaces an earlier one, so default arguments or argument handling are commonly used.

7. Abstract Classes and Interfaces

An abstract class defines a common abstraction and may contain both implemented and unimplemented methods. It cannot normally be instantiated directly.

An interface specifies a contract that implementing classes agree to provide. Java interfaces may contain abstract methods, default methods, static methods, and constants. A class can implement multiple interfaces. C++ has no separate interface keyword; a class containing pure virtual functions is commonly used as an interface. Python may use protocols, abstract base classes, or informal duck typing.

Use an abstract class when related classes share state or implementation. Use an interface when the important idea is a capability or contract that may be implemented by otherwise unrelated classes.

Java example:

interface Payable {
    double amountDue();
}

class Invoice implements Payable {
    private final double amount;

    Invoice(double amount) {
        if (amount < 0) throw new IllegalArgumentException();
        this.amount = amount;
    }

    @Override
    public double amountDue() {
        return amount;
    }
}

A client can work with Payable rather than depending on Invoice:

static double total(Payable[] items) {
    double result = 0;
    for (Payable item : items) {
        result += item.amountDue();
    }
    return result;
}

This supports the Dependency Inversion Principle: high-level policy should depend on abstractions rather than concrete implementation details.

8. Core Design Principles

Several principles guide maintainable object-oriented software.

Single Responsibility Principle

A class should have one coherent responsibility and one main reason to change. A class that validates orders, stores them, sends emails, and generates reports is likely carrying several responsibilities.

Open–Closed Principle

Software entities should be open for extension but closed for unnecessary modification. Polymorphism and composition can allow a new payment method to be added without rewriting stable order-processing logic.

Liskov Substitution Principle

Subtypes must preserve the expectations established by their base type. A subtype should not unexpectedly reject valid inputs, weaken guarantees, or change essential meaning.

Interface Segregation Principle

Clients should not be forced to depend on methods they do not use. Several small, focused interfaces are often better than one large interface.

Dependency Inversion Principle

Depend on abstractions, not volatile concrete details. Dependency injection makes dependencies explicit and improves testing.

These principles are guidelines, not mechanical laws. Excessive abstraction can make a small program harder to understand. The appropriate design depends on system size, change patterns, team skills, and operational requirements.

9. Worked Application: A Library Lending System

Background

Consider an illustrative library system that lends books and electronic media to registered members. The system must record loans, calculate due dates, and support different lending rules.

Problem analysis

Important domain concepts include Member, Item, Book, EBook, and Loan. A member may have several loans. A loan refers to one item and one member. A book and an electronic book share common descriptive data but may have different lending rules.

A first design might create a large Library class containing all data and rules. This creates poor cohesion and makes new media types difficult to add. A better design separates responsibilities.

Approach

An abstract LibraryItem can define common operations:

abstract class LibraryItem {
    private final String id;
    private final String title;

    protected LibraryItem(String id, String title) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException();
        if (title == null || title.isBlank()) throw new IllegalArgumentException();
        this.id = id;
        this.title = title;
    }

    public String id() { return id; }
    public String title() { return title; }
    public abstract int loanPeriodDays();
}

class Book extends LibraryItem {
    public Book(String id, String title) {
        super(id, title);
    }

    @Override
    public int loanPeriodDays() { return 21; }
}

class EBook extends LibraryItem {
    public EBook(String id, String title) {
        super(id, title);
    }

    @Override
    public int loanPeriodDays() { return 14; }
}

A Loan should contain a LibraryItem reference rather than testing its concrete type with a long chain of conditionals. The loan service can call loanPeriodDays() polymorphically.

class Loan {
    private final LibraryItem item;
    private final java.time.LocalDate startDate;

    public Loan(LibraryItem item, java.time.LocalDate startDate) {
        this.item = item;
        this.startDate = startDate;
    }

    public java.time.LocalDate dueDate() {
        return startDate.plusDays(item.loanPeriodDays());
    }
}

Outcome

Adding an AudioBook requires a new subclass with its own loan-period rule. Existing Loan code remains unchanged. The design also permits unit tests to use a simple test implementation of LibraryItem.

Learning points

  1. The abstraction captures a stable concept shared by multiple item types.
  2. Inheritance is used because each item is substitutable as a LibraryItem.
  3. Polymorphism removes type-specific conditional logic.
  4. Encapsulation protects identifiers and dates from arbitrary modification.
  5. Composition is used when a Loan contains an item reference rather than becoming an item itself.
  6. A production system would also need persistence, authentication, concurrency control, overdue policies, accessibility, and privacy protection.

10. Common Errors and Limitations

Anemic domain models

A class with only public fields and no meaningful behaviour may fail to protect its invariants. It can be appropriate for a simple data-transfer object, but it is not automatically a good domain model.

Deep inheritance hierarchies

Many levels of inheritance make behaviour difficult to trace. Changes near the top may affect numerous subclasses. Prefer shallow, stable hierarchies and composition where it better represents variation.

Inappropriate inheritance

A class should not inherit merely to reuse a few methods. If the subtype cannot honour the base class contract, use composition or a separate abstraction.

Excessive getters and setters

Unrestricted accessors can expose implementation details and permit invalid state. Expose operations that express domain rules.

Excessive use of global mutable objects

Global state creates hidden dependencies and makes tests order-dependent. Pass dependencies explicitly or use controlled dependency injection.

Confusing identity and equality

Two objects may contain equal values but still represent different entities. For example, two account records might have equal names and balances but different identities. Languages also differ in equality semantics: Java distinguishes reference comparison from .equals(), Python distinguishes is from ==, and C++ requires appropriately defined comparison operations.

Resource and exception errors

Garbage collection does not eliminate the need to close files, release locks, or handle transactions. Exceptions should be handled at a level that can take meaningful action, not indiscriminately hidden.

11. Testing, Security, and Professional Practice

Object-oriented designs should be tested at several levels. Unit tests verify class invariants and method behaviour. Contract tests verify that implementations satisfy an interface. Integration tests verify collaboration among objects.

Encapsulation supports testing by allowing a class to validate its own state. Dependency injection allows external services such as databases or payment providers to be replaced by controlled test doubles.

Security is also a design concern. A private field is not a complete security boundary; compiled programs, reflection, debugging tools, and application interfaces may expose information. Sensitive data should be minimised, encrypted where appropriate, protected by authentication and authorisation, and excluded from logs unless there is a justified operational need.

When AI-assisted programming tools generate classes or inheritance structures, developers remain responsible for correctness, licensing compliance, security, privacy, and maintainability. Generated code should be reviewed, tested, and checked for hidden assumptions. An AI tool may produce plausible but inappropriate inheritance, expose confidential attributes, or omit validation.

12. Contemporary Language Idioms

Modern C++ favours standard containers, RAII, smart pointers, const correctness, move semantics where useful, and algorithms from the standard library. Raw owning pointers should generally be avoided.

Modern Java favours immutable fields where practical, records for simple immutable data carriers, interfaces for stable contracts, and try-with-resources for resource management. Excessive framework-generated classes should not replace clear domain reasoning.

Modern Python favours small classes, properties when controlled access is needed, context managers for resources, type hints for communication and tooling, dataclasses for suitable data-oriented objects, and protocols when structural compatibility is more useful than inheritance.

Across all three languages, good idioms include meaningful names, small cohesive methods, explicit dependencies, validation at boundaries, automated tests, documentation of public contracts, and refactoring guided by actual requirements rather than speculative abstraction.

Challenges and Limitations

OOP is not universally superior. Procedural or functional designs may be clearer for transformations, numerical algorithms, and small scripts. Object-oriented systems can introduce indirection, boilerplate, memory overhead, and complex aliasing. Inheritance may make variation harder rather than easier when the hierarchy is unstable.

The correct question is not “How can every noun become a class?” but “Which responsibilities, relationships, and changes should the design make clear?” A professional developer selects object orientation where it improves understanding, changeability, reliability, or reuse.

Chapter Summary

Object-oriented programming models software as collaborating objects that combine state and behaviour. Classes define the structure and operations of objects, while constructors establish valid initial state. Encapsulation and information hiding protect invariants and reduce dependency on implementation details.

Association, aggregation, and composition describe different relationships between objects. Inheritance can support reuse and substitutability, but it should represent a genuine behavioural relationship. Composition is often more flexible for assembling changing behaviours.

Polymorphism allows common interfaces to operate on different implementations. Compile-time polymorphism includes overloading, while run-time polymorphism uses overriding and dynamic dispatch. Abstract classes and interfaces express shared contracts, and design principles such as single responsibility, substitution, interface segregation, and dependency inversion support maintainable systems.

C++, Java, and Python share core OOP ideas but differ in memory management, access control, typing, resource handling, and idiomatic style. Effective object-oriented development also requires testing, secure handling of data, careful use of AI-generated code, and judgement about when OOP is not the best approach.

Key Terms

Class: A definition describing the data and operations of objects.

Object: A concrete instance of a class.

Attribute: Data associated with an object.

Method: An operation defined by a class.

State: The current values of an object’s attributes.

Constructor: An operation used to establish an object’s initial valid state.

Destructor: A C++ operation used when an object’s lifetime ends.

Encapsulation: Bundling state and behaviour while controlling access.

Information hiding: Concealing implementation decisions from clients.

Association: A relationship in which objects know or use one another.

Aggregation: A whole–part relationship whose parts can exist independently.

Composition: A strong whole–part relationship involving ownership or dependent lifetime.

Inheritance: Deriving a new class from an existing class.

Polymorphism: Allowing one interface to represent or invoke different implementations.

Overloading: Defining operations with the same name but different parameters.

Overriding: Replacing inherited behaviour in a subtype.

Abstract class: A non-instantiable class representing a partial abstraction.

Interface: A contract describing operations a class agrees to provide.

Invariant: A condition that must remain true for a valid object.

Dependency injection: Supplying an object’s dependencies from outside rather than constructing them internally.

Review Questions

Short Answer Questions

  1. Distinguish between a class and an object.
  2. What is the purpose of a constructor?
  3. Explain the difference between encapsulation and information hiding.
  4. Define method overriding and method overloading.
  5. Why is composition often preferred to inheritance?

Long Answer Questions

  1. Explain how encapsulation protects class invariants, using a bank account as an example.
  2. Compare inheritance, aggregation, and composition, and give an example of each.
  3. Explain compile-time and run-time polymorphism in C++, Java, and Python.
  4. Discuss abstract classes and interfaces, including situations in which each is appropriate.
  5. Describe the SOLID principles and explain how two of them improve maintainability.

Analytical / Application Questions

  1. Design an object-oriented model for an online shopping cart. Identify classes, responsibilities, relationships, and at least one suitable interface.
  2. A proposed Bird hierarchy contains a FlyingBird superclass and a Penguin subclass. Analyse whether this design satisfies substitutability and propose a better model.
  3. Refactor a program in which one Library class stores members, calculates fines, writes database records, and sends email notifications. Identify design problems and propose a composition-based solution.

Multiple Choice Questions with Answers

Question 1

Which statement best describes encapsulation?

A. Combining data and operations while controlling access to internal state

B. Copying every method from a superclass into a subclass

C. Converting all objects into primitive values

D. Calling a method without creating an object

Correct Answer: A. Combining data and operations while controlling access to internal state

Question 2

What is the primary purpose of a constructor?

A. To destroy an object immediately after creation

B. To establish a valid initial state for an object

C. To make every method static

D. To prevent all inheritance

Correct Answer: B. To establish a valid initial state for an object

Question 3

Which relationship most strongly represents ownership of a part by a whole?

A. Association

B. Aggregation

C. Composition

D. Overloading

Correct Answer: C. Composition

Question 4

What does run-time polymorphism usually select?

A. The implementation based on the actual object at execution time

B. The variable name based only on spelling

C. The constructor with the fewest statements

D. The operating system used to compile the program

Correct Answer: A. The implementation based on the actual object at execution time

Question 5

Which principle requires a subtype to remain usable wherever its base type is expected?

A. Single Responsibility Principle

B. Liskov Substitution Principle

C. Interface Segregation Principle

D. Dependency Inversion Principle

Correct Answer: B. Liskov Substitution Principle

Question 6

In Java, which keyword indicates that a class provides the operations specified by an interface?

A. extends

B. inherits

C. implements

D. virtual

Correct Answer: C. implements

Question 7

Why should a C++ polymorphic base class commonly have a virtual destructor?

A. To permit correct destruction through a base-class pointer

B. To make all fields public

C. To overload every constructor

D. To prevent method overriding

Correct Answer: A. To permit correct destruction through a base-class pointer

Question 8

Which Python practice is preferred for reliably closing a file?

A. Relying only on object destruction

B. Using a with statement or context manager

C. Making the filename a global variable

D. Inheriting every file class from list

Correct Answer: B. Using a with statement or context manager

Question 9

A class that validates orders, writes database records, sends emails, and generates reports most clearly violates which principle?

A. Single Responsibility Principle

B. Liskov Substitution Principle

C. Open–Closed Principle only

D. The principle of identity

Correct Answer: A. Single Responsibility Principle

Question 10

Which design choice best supports dependency inversion?

A. Constructing every concrete service inside the high-level class

B. Depending on a stable interface and supplying the implementation externally

C. Making all fields public

D. Replacing all methods with global variables

Correct Answer: B. Depending on a stable interface and supplying the implementation externally

Programming in C++/Java/Python
Chapter 7 of 15