Programming Fundamentals and Object-Oriented Programming

Chapter 8
Abstraction, Inheritance, Interfaces and Polymorphism

Post Graduate Diploma in Full Stack Development
Programming Fundamentals and Object-Oriented Programming   •   Post Graduate

8. Abstraction, Inheritance, Interfaces and Polymorphism

Introduction and Context

Object-oriented programming (OOP) becomes especially powerful when software models contain families of related objects, interchangeable services, and behaviours that vary according to context. Abstraction, inheritance, interfaces, and polymorphism provide the principal mechanisms for designing such systems. They allow developers to express what an object can do without exposing every implementation detail, reuse common behaviour, substitute one implementation for another, and extend systems without rewriting existing client code.

These mechanisms are closely related but not interchangeable. Abstraction identifies the essential characteristics of an entity. Inheritance creates a relationship in which one class derives from another. An interface specifies a behavioural contract. Polymorphism allows a common operation to produce different results according to the actual object receiving the message. Sound design requires more than knowing the syntax of these features. It requires judgment about coupling, cohesion, substitutability, maintainability, and the trade-off between inheritance and composition.

The examples in this chapter use Java-like syntax because it clearly illustrates abstract classes, interfaces, overriding, and dynamic dispatch. The underlying ideas also apply to C#, Kotlin, TypeScript, Python, C++, and other object-oriented languages, although syntax and type-system rules differ.

Learning Objectives

After studying this chapter, you should be able to:

  • explain abstraction, inheritance, interfaces, overriding, overloading, and polymorphism;
  • distinguish abstract classes from interfaces and select between them appropriately;
  • implement inheritance hierarchies and interface-based designs;
  • describe how dynamic dispatch determines the method executed at runtime;
  • evaluate designs using coupling, cohesion, and the Liskov Substitution Principle;
  • compare inheritance with composition and delegation;
  • identify common design failures, including fragile base classes and inappropriate inheritance;
  • apply these ideas to maintainable full-stack software systems.

Abstraction: Modelling the Essential

Abstraction is the deliberate representation of the important features of a concept while suppressing details that are not necessary at a particular level of use. A payment service may expose `authorize(amount)` and `refund(transactionId)` while hiding network protocols, encryption, retries, and database transactions. A programmer using the service needs the contract, not its internal algorithm.

Abstraction operates at several levels. A method abstracts an algorithm behind a name. A class abstracts state and operations into a coherent unit. An abstract class captures common structure for a family of objects without representing a complete object on its own. An interface abstracts a capability or contract independently of implementation.

Good abstraction is selective rather than total. Hiding every detail can make a system difficult to understand or configure. Exposing every detail creates excessive coupling. The designer must decide which decisions belong behind a boundary and which must remain visible to clients.

Consider a notification system. Clients should depend on a `Notifier` contract rather than on the internal details of SMTP, an SMS gateway, or a push-notification provider:

```java

interface Notifier {

void send(String recipient, String message);

}

```

The interface communicates the service that clients require. It does not dictate how the message is transmitted. This separation enables testing with a fake notifier and replacing a provider without changing business logic.

Abstract Classes and Partial Implementation

An abstract class is a class intended to serve as a base for other classes. It may contain fields, constructors, concrete methods, and abstract methods. An abstract method declares required behaviour without providing an implementation. Because an abstract class is incomplete, it cannot normally be instantiated directly.

```java

abstract class Report {

private final String title;

protected Report(String title) {

this.title = title;

}

public String getTitle() {

return title;

}

public abstract String generate();

public void print() {

System.out.println(generate());

}

}

class SalesReport extends Report {

public SalesReport(String title) {

super(title);

}

@Override

public String generate() {

return "Sales data for " + getTitle();

}

}

```

The base class centralizes information common to all reports and establishes an invariant: every report has a title and can be generated. Subclasses provide the variable part. This is often described as the Template Method style when a base method defines an overall algorithm and delegates selected steps to subclass methods.

An abstract class is suitable when related classes share implementation, state, constructors, or protected helper methods. It also expresses a strong conceptual relationship. However, inheritance creates dependency: changes to the base class can affect every subclass. Protected state may be altered in ways that violate assumptions made by the parent, and a subclass can become tied to implementation details rather than merely to a stable contract.

Interfaces as Contracts and Capabilities

An interface describes operations that an implementing class promises to provide. It is most useful when the relationship is behavioural rather than taxonomic. A class can implement multiple interfaces, allowing it to participate in several abstractions without inheriting implementation from several parents.

```java

interface Exportable {

String export(String format);

}

class Invoice implements Exportable {

@Override

public String export(String format) {

return "Invoice exported as " + format;

}

}

```

Interfaces support dependency inversion. Instead of a `CheckoutService` constructing a specific `StripePaymentGateway`, it can receive a `PaymentGateway` interface. The service then depends on a stable abstraction, while production, testing, and alternative deployments provide different implementations.

Modern languages may permit default or static interface methods. Such features can provide limited reusable behaviour, but an interface should primarily represent a focused contract. A large interface containing unrelated operations forces clients to depend on methods they do not need. This problem is commonly associated with the Interface Segregation Principle.

An interface can express a role such as `Sortable`, `Cacheable`, `Auditable`, or `Authenticatable`. These roles need not correspond to a single inheritance hierarchy. The distinction between “is a kind of” and “can perform” is useful: a `Document` may be `Printable`, while a `Printer` may also be `Printable`, even though the two objects are not members of the same conceptual family.

Inheritance and the Meaning of “Is a”

Inheritance allows a subclass to acquire accessible members of a superclass and specialize or extend them. If `SavingsAccount` extends `Account`, the model asserts that a savings account is an account and should be usable wherever an account is expected.

```java

class Account {

protected double balance;

public void deposit(double amount) {

if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");

balance += amount;

}

public double getBalance() {

return balance;

}

}

class SavingsAccount extends Account {

private final double interestRate;

public SavingsAccount(double interestRate) {

this.interestRate = interestRate;

}

public void addInterest() {

balance += balance * interestRate;

}

}

```

Inheritance can reduce duplication and provide a natural vocabulary for related concepts. Nevertheless, it should not be selected merely because two classes share fields. A common data structure does not necessarily imply a substitutable conceptual relationship. For example, a `Report` and a `DatabaseConnection` might both have a `name`, but that does not justify inheritance.

Deep inheritance trees are difficult to understand because behaviour is distributed across several levels. They can also produce the fragile base-class problem: an apparently harmless change to a base class changes the assumptions or behaviour of subclasses. A base class should therefore expose a carefully designed contract, preserve its invariants, and avoid requiring subclasses to understand unnecessary internal details.

Overriding, Overloading, and Dynamic Dispatch

Overriding occurs when a subclass supplies a new implementation for an inherited method with the same signature. The `@Override` annotation is valuable because the compiler can detect spelling or signature errors. Overloading, by contrast, means defining multiple methods with the same name but different parameter lists. Overloading is generally resolved at compile time; overriding participates in runtime polymorphism.

```java

class TextFormatter {

public String format(Object value) {

return String.valueOf(value);

}

}

class JsonFormatter extends TextFormatter {

@Override

public String format(Object value) {

return "{\"value\":\"" + value + "\"}";

}

}

```

Dynamic dispatch means that when an overridden instance method is called through a superclass or interface reference, the implementation associated with the actual runtime object is selected.

```java

TextFormatter formatter = new JsonFormatter();

System.out.println(formatter.format("data"));

```

The declared type of `formatter` is `TextFormatter`, but the runtime object is a `JsonFormatter`; therefore, `JsonFormatter.format` executes. This behaviour allows client code to depend on a general abstraction while implementations vary.

Dynamic dispatch does not mean that every operation is dynamically selected. Static methods, private methods, constructors, and fields do not participate in ordinary instance-method overriding in the same way. A variable’s declared type also controls which members are available at compile time, even when the runtime object is a subclass.

Polymorphism in System Design

Polymorphism means “many forms.” In OOP, it commonly refers to treating different objects through a shared superclass or interface while allowing each object to respond according to its own implementation.

```java

interface TaxRule {

double calculate(double amount);

}

class StandardTax implements TaxRule {

public double calculate(double amount) {

return amount * 0.18;

}

}

class ReducedTax implements TaxRule {

public double calculate(double amount) {

return amount * 0.05;

}

}

class InvoiceService {

public double totalTax(List<TaxRule> rules, double amount) {

double total = 0;

for (TaxRule rule : rules) {

total += rule.calculate(amount);

}

return total;

}

}

```

`InvoiceService` does not contain a chain of conditions identifying every tax category. New tax rules can be added as new implementations. This reduces conditional complexity and supports the Open–Closed Principle: software entities should be open to extension but closed to unnecessary modification.

Polymorphism may be subtype-based, as in inheritance and interfaces, or achieved through composition and strategy objects. The latter is often preferable when behaviour changes independently of an object’s identity. A `ShippingOrder` can receive a `ShippingCostStrategy` rather than acquire a large hierarchy of order subclasses.

Substitutability, Coupling, and Cohesion

Polymorphism is valuable only when substitution is safe. The Liskov Substitution Principle states that objects of a subtype should be usable wherever objects of the supertype are expected without changing the correctness of the client program. A subtype may strengthen internal implementation, but it should not unexpectedly weaken guarantees or introduce incompatible behaviour.

A classic design failure arises when a subtype cannot support an operation promised by its parent. If `Bird` defines `fly()`, then a `Penguin` inheriting from `Bird` may be forced to reject a valid-looking operation. The problem is not that penguins are unusual; it is that the abstraction is poorly chosen. A better design might define `Bird` separately from `Flyable`.

Substitutability involves more than matching method signatures. A subtype should respect:

  • preconditions: it should not demand more than the parent contract;
  • postconditions: it should deliver at least the promised result;
  • invariants: it should preserve essential rules of the parent abstraction;
  • behavioural expectations: exceptions, side effects, and performance assumptions should remain reasonable.

Coupling measures how strongly one module depends on another. Inheritance often creates tighter coupling than interface-based composition because subclasses depend on inherited structure and lifecycle. Cohesion measures how closely the responsibilities within a module belong together. A focused class with one coherent purpose generally has stronger cohesion than a class combining storage, payment, reporting, and notification.

A maintainable design generally seeks low unnecessary coupling and high cohesion. These are not absolute rules: some coupling is required to express collaboration, and excessive fragmentation can make a system harder to understand. The objective is controlled dependency, not the complete absence of dependency.

Composition Over Inheritance

Composition constructs an object from other objects and delegates responsibilities to them. Inheritance says “is a”; composition often says “has a” or “uses a.”

```java

interface DiscountPolicy {

double apply(double price);

}

class NoDiscount implements DiscountPolicy {

public double apply(double price) { return price; }

}

class ShoppingCart {

private DiscountPolicy policy;

public ShoppingCart(DiscountPolicy policy) {

this.policy = policy;

}

public void setPolicy(DiscountPolicy policy) {

this.policy = policy;

}

public double finalPrice(double price) {

return policy.apply(price);

}

}

```

The cart is not a subtype of every discount policy. It collaborates with one. A promotional policy can be replaced at runtime, tested independently, or configured through dependency injection. Composition usually provides greater flexibility and limits the impact of change. Its costs include additional objects, indirection, and the need to define forwarding or delegation clearly.

Inheritance remains appropriate when the hierarchy is conceptually stable, substitutability is strong, and shared implementation genuinely belongs to the abstraction. Composition is often preferable when behaviour varies independently, when multiple combinations are required, or when reuse is the primary motivation rather than subtype modelling.

A Practical Design Method

When designing a polymorphic component, begin with responsibilities rather than classes. Identify what clients need to request and what guarantees they require. Extract a small interface when multiple implementations are plausible. Use an abstract class when common state and implementation are central to a genuine family of objects.

Next, test the proposed hierarchy with substitutability questions: Can every subtype honour the parent’s contract? Does any subtype need to disable, ignore, or reinterpret an inherited operation? If so, revise the abstraction. Then examine dependency direction. Business rules should generally depend on interfaces or domain abstractions rather than infrastructure classes.

Finally, assess change scenarios. If adding a new variation requires editing a large conditional statement, polymorphism may improve the design. If adding a new subtype causes changes throughout the system, the abstraction may be leaking implementation assumptions. Unit tests should verify both individual implementations and the contract shared by all implementations.

Illustrative Case: Refactoring a Delivery Platform

Background

A fictional company, SwiftCart, operates an online marketplace. Its first delivery module uses a `DeliveryService` class containing a long conditional statement for bicycle, van, express, and international deliveries. Every new delivery option requires modifying the same method.

Situation and Problem

The method has grown difficult to test. It calculates cost, validates destination rules, selects a carrier, and sends notifications. The class has low cohesion and high coupling to carrier-specific APIs. A change for international delivery unexpectedly breaks local delivery tests.

Analysis

The delivery types share a broad business goal, but their pricing algorithms and carrier integrations vary independently. Creating a deep inheritance tree would not solve the problem if the service continued to own unrelated responsibilities. The design needs abstractions for pricing, carrier selection, and notification.

Approach and Solution

SwiftCart introduces focused interfaces:

```java

interface DeliveryPricing {

Money calculate(Order order);

}

interface Carrier {

TrackingId book(Order order);

}

interface DeliveryNotifier {

void notifyCustomer(Order order, TrackingId trackingId);

}

```

`DeliveryService` receives these collaborators through its constructor. Bicycle and van pricing become separate implementations. Carrier adapters hide external API details. Test doubles implement the same interfaces. Polymorphism allows the service to execute the appropriate implementation without a growing conditional statement.

An abstract `BaseCarrierAdapter` is retained only where all carrier adapters share authentication and retry logic. Carrier-specific booking remains abstract. This combines composition for business variation with limited inheritance for genuinely shared infrastructure behaviour.

Outcome

The service becomes easier to test and extend. A new carrier can be added without changing pricing rules. A new pricing policy can be tested independently. The design does not eliminate all complexity; it places complexity behind explicit boundaries and makes dependencies visible.

Learning Points

The case demonstrates that polymorphism is a design technique, not merely a language feature. Interfaces express contracts, composition controls variation, and carefully limited inheritance can reuse stable implementation. Cohesion improves when each component has a focused responsibility, while coupling decreases when clients depend on abstractions rather than vendor details.

Common Errors, Risks, and Limitations

A frequent error is using inheritance solely to reuse code. This may create a false subtype relationship and expose methods that do not make sense. Another is placing substantial mutable state in a base class, allowing subclasses to violate invariants. Deep hierarchies, overriding methods with surprising side effects, and calling overridable methods from constructors are additional risks.

Interface design can also fail. A “god interface” forces implementations to provide irrelevant operations. Excessive abstraction can create needless indirection and make simple code harder to follow. Polymorphism is not always superior to a conditional; a small, stable conditional may be clearer than a hierarchy of one-method classes.

Developers should also consider equality, serialization, security, and persistence. A subclass may require different validation or expose data that the parent never anticipated. APIs that permit arbitrary implementations need documented contracts, versioning policies, and compatibility tests.

Professional and Ethical Considerations

Abstractions influence who can change a system safely. Clear interfaces and substitutable implementations support team collaboration and reduce the risk that one developer’s change silently damages another component. Public APIs should document preconditions, exceptions, side effects, thread-safety expectations, and data-handling responsibilities.

In systems processing payments, identity, or personal data, an interface boundary must not be mistaken for a security boundary. Authorization, audit logging, encryption, and validation still require explicit implementation. A polymorphic plugin mechanism can increase extensibility but may also permit untrusted code or inappropriate data access. Governance should therefore include dependency review, access control, testing, and version management.

Contemporary Development Practices

Dependency injection frameworks use interfaces and composition to assemble implementations at runtime. Modular architectures and microservices apply similar ideas at larger boundaries: clients depend on contracts while services evolve behind them. Contract testing verifies that an implementation satisfies the expectations of its consumers.

Modern statically typed languages provide features such as sealed classes, records, traits, extension methods, and default interface implementations. Sealed hierarchies can be valuable when the set of permitted variants is intentionally closed, while open interfaces are useful when external implementations are expected. In dynamically typed languages, polymorphism may rely on duck typing, but explicit protocols, tests, and documentation remain important.

Automation tools can analyse inheritance depth, coupling, unused interfaces, and code duplication. Static analysis is useful evidence, not a substitute for design reasoning. A low numerical coupling score does not guarantee that the abstraction reflects the domain correctly.

Best Practices

  • Model stable contracts before selecting implementation mechanisms.
  • Prefer small, cohesive interfaces over broad interfaces with unrelated operations.
  • Use inheritance only when the subtype relationship is meaningful and substitutable.
  • Prefer composition when behaviour varies independently or must change at runtime.
  • Mark overriding methods explicitly and test shared behavioural contracts.
  • Keep base classes responsible for stable invariants; avoid exposing fragile protected state.
  • Depend on abstractions and inject infrastructure dependencies.
  • Use polymorphism to remove genuine variation, not to disguise simple logic.
  • Document exceptions, side effects, lifecycle rules, and compatibility expectations.
  • Review designs for coupling, cohesion, extensibility, and operational risk.

Chapter Summary

Abstraction hides unnecessary detail while preserving a usable contract. Abstract classes provide shared state and partial implementation for closely related types; interfaces define focused capabilities and support dependency inversion. Inheritance expresses a subtype relationship, whereas composition assembles behaviour through collaboration. Overriding replaces inherited behaviour, and dynamic dispatch selects the implementation belonging to the runtime object. Polymorphism enables clients to work with abstractions while implementations vary.

Effective use of these mechanisms depends on substitutability, cohesion, and controlled coupling. The Liskov Substitution Principle warns against hierarchies whose subtypes cannot honour parent expectations. Composition is frequently more flexible than inheritance, although carefully designed inheritance remains valuable. Professional designs combine these tools with testing, dependency injection, explicit contracts, security controls, and thoughtful evaluation of change scenarios.

Key Terms

Abstraction: Representation of essential features while hiding unnecessary implementation detail.

Abstract class: A non-complete class intended to provide common structure or behaviour for subclasses.

Abstract method: A method declaration without an implementation that subclasses are expected to provide.

Interface: A contract specifying operations that implementing classes must support.

Inheritance: A mechanism through which a subclass derives accessible structure and behaviour from a superclass.

Subclass: A class derived from another class and potentially usable as its parent type.

Superclass: A class from which another class inherits.

Overriding: Replacing an inherited method implementation with a subtype-specific implementation.

Overloading: Defining methods with the same name but different parameter lists.

Dynamic dispatch: Runtime selection of an overridden method according to the actual object type.

Polymorphism: The ability to use different implementations through a common abstraction.

Composition: Building an object by combining collaborating objects or services.

Delegation: Passing responsibility for an operation to another object.

Coupling: The degree of dependency between software components.

Cohesion: The degree to which the responsibilities of a component belong together.

Liskov Substitution Principle: The requirement that subtypes remain usable wherever their supertypes are expected.

Dependency inversion: Designing high-level policy to depend on abstractions rather than concrete low-level details.

Contract: A documented set of operations, guarantees, constraints, and behavioural expectations.

Template Method: A design approach in which a base algorithm defines steps implemented or specialized by subclasses.

Review Questions

Short Answer Questions

1. What is the main purpose of abstraction in object-oriented design?

2. How does an interface differ from an abstract class?

3. Distinguish method overriding from method overloading.

4. What is dynamic dispatch?

5. Define coupling and cohesion.

Long Answer Questions

1. Explain how abstract classes support partial implementation, using a suitable example.

2. Discuss the role of interfaces in dependency inversion and testable software.

3. Explain inheritance, polymorphism, and substitutability as related but distinct concepts.

4. Compare composition and inheritance, including situations in which each is appropriate.

5. Analyse the Liskov Substitution Principle and explain how violating it damages a class hierarchy.

Analytical / Application Questions

1. A `FileReport`, `WebReport`, and `DatabaseReport` share a title but have different generation processes. Propose an appropriate abstraction and justify whether you would use an interface, abstract class, or composition.

2. A payment class contains a large conditional statement for card, bank-transfer, and wallet payments. Redesign the situation using polymorphism and explain the expected effect on coupling and extensibility.

3. A subclass overrides a parent method but rejects some inputs that the parent accepts. Analyse whether this violates substitutability and recommend a design correction.

22. Multiple Choice Questions with Answers

Question 1

Which situation most strongly justifies using an abstract class rather than only an interface?

A. Unrelated classes need to share a capability name

B. Closely related classes need common state, constructors, and partial implementation

C. A class must support multiple independent roles

D. A client should depend on a vendor-neutral contract

Correct Answer: B. Closely related classes need common state, constructors, and partial implementation

Question 2

What determines which overridden instance method executes in a polymorphic call?

A. The variable name

B. The order in which classes were compiled

C. The runtime type of the object

D. The number of fields in the superclass

Correct Answer: C. The runtime type of the object

Question 3

Which statement best describes composition?

A. A subclass inherits all behaviour from a superclass

B. An object collaborates with other objects to obtain behaviour

C. A compiler selects among overloaded methods at runtime

D. A class hides every public operation

Correct Answer: B. An object collaborates with other objects to obtain behaviour

Question 4

A Penguin inherits from Bird, but Bird promises that all birds can fly. What design issue is most directly illustrated?

A. Excessive method overloading

B. Violation of substitutability caused by an unsuitable abstraction

C. Insufficient database normalization

D. Failure of static dispatch

Correct Answer: B. Violation of substitutability caused by an unsuitable abstraction

Question 5

Which design most directly supports dependency inversion?

A. A business service constructs a concrete payment gateway internally

B. A business service receives a PaymentGateway interface implementation

C. Every subclass accesses protected infrastructure fields

D. A controller contains all provider-specific logic

Correct Answer: B. A business service receives a PaymentGateway interface implementation

Question 6

What is the principal difference between overriding and overloading?

A. Overriding changes an inherited implementation; overloading changes the parameter list

B. Overriding occurs only in interfaces; overloading occurs only in abstract classes

C. Overriding is always compile-time; overloading is always runtime

D. There is no meaningful difference

Correct Answer: A. Overriding changes an inherited implementation; overloading changes the parameter list

Question 7

Which design choice is most likely to improve cohesion?

A. Combining payment, reporting, storage, and notification in one class

B. Creating a focused pricing policy with responsibility for price calculation

C. Adding every possible operation to one interface

D. Moving all behaviour into a deep superclass

Correct Answer: B. Creating a focused pricing policy with responsibility for price calculation

Question 8

Why can a deep inheritance hierarchy create maintenance risk?

A. It prevents all forms of code reuse

B. Changes in a base class may affect assumptions and behaviour in many subclasses

C. It makes interfaces impossible to declare

D. It eliminates dynamic dispatch

Correct Answer: B. Changes in a base class may affect assumptions and behaviour in many subclasses

Question 9

In the SwiftCart case, why were focused interfaces such as DeliveryPricing and Carrier useful?

A. They forced every delivery type to implement unrelated operations

B. They separated independently varying responsibilities behind explicit contracts

C. They eliminated the need for testing

D. They guaranteed that no implementation could change

Correct Answer: B. They separated independently varying responsibilities behind explicit contracts

Question 10

Which statement about polymorphism is most accurate?

A. It means every class must inherit from one universal base class

B. It allows different implementations to be used through a shared abstraction

C. It replaces the need for contracts and documentation

D. It always produces simpler code than a conditional

Correct Answer: B. It allows different implementations to be used through a shared abstraction

Programming Fundamentals and Object-Oriented Programming
Chapter 8 of 15