Object-Oriented Programming
Abstract Classes
Software Development
Programming Best Practices
OOP Principles

When to use abstract classes?

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Abstract classes are useful when related types must share both a common contract and reusable implementation. They help enforce workflow invariants while allowing subclass-specific behavior at defined extension points. The right decision depends on whether your problem needs shared stateful logic or only a capability interface.

What Abstract Classes Provide

An abstract class can include:

  • abstract methods that subclasses must implement
  • concrete methods with shared behavior
  • protected helper methods and fields
  • constructors for shared initialization

This combination makes abstract classes suitable for template-style workflows.

java
1public abstract class ReportGenerator {
2
3    public final String generate() {
4        String raw = fetchData();
5        String cleaned = clean(raw);
6        return format(cleaned);
7    }
8
9    protected String clean(String raw) {
10        return raw.trim();
11    }
12
13    protected abstract String fetchData();
14    protected abstract String format(String input);
15}

generate defines invariant flow, while subclasses customize fetch and format behavior.

When Abstract Classes Are a Good Fit

Choose an abstract class when:

  1. subclasses are conceptually in one family
  2. meaningful implementation must be shared
  3. extension points should be controlled
  4. common initialization logic belongs in one place
java
1public class ApiReportGenerator extends ReportGenerator {
2    @Override
3    protected String fetchData() {
4        return " api payload ";
5    }
6
7    @Override
8    protected String format(String input) {
9        return "API:" + input;
10    }
11}
12
13public class FileReportGenerator extends ReportGenerator {
14    @Override
15    protected String fetchData() {
16        return " file payload ";
17    }
18
19    @Override
20    protected String format(String input) {
21        return "FILE:" + input;
22    }
23}

Both implementations reuse orchestration and cleaning logic, reducing duplication.

When Interface Is Better

If you only need a contract without shared stateful behavior, prefer interfaces.

java
public interface Exporter {
    String export();
}

Interface-first design is often better when:

  • implementations are from unrelated domains
  • multiple inheritance of behavior contracts is needed
  • you want low coupling and flexible composition

Modern Java default interface methods can provide small shared helpers without forcing inheritance hierarchy.

Abstract Class Versus Composition

Inheritance is not always the best tool. If behavior varies by runtime policy or changes often, composition with strategy objects can be cleaner.

java
1public interface TaxPolicy {
2    double apply(double amount);
3}
4
5public class StandardTaxPolicy implements TaxPolicy {
6    @Override
7    public double apply(double amount) {
8        return amount * 1.13;
9    }
10}
11
12public class InvoiceService {
13    private final TaxPolicy taxPolicy;
14
15    public InvoiceService(TaxPolicy taxPolicy) {
16        this.taxPolicy = taxPolicy;
17    }
18
19    public double total(double subtotal) {
20        return taxPolicy.apply(subtotal);
21    }
22}

Composition avoids deep inheritance chains and keeps changes localized.

Practical Decision Checklist

Use abstract class when:

  • shared algorithm skeleton is required
  • protected reusable internals make design clearer
  • constructor-level shared setup is necessary

Use interface or composition when:

  • shared implementation is minimal
  • multiple independent behaviors must be mixed
  • hierarchy depth would increase complexity

This checklist helps prevent overusing inheritance.

Testing Implications

Abstract-class designs benefit from two testing layers:

  • base-class contract tests that verify invariant workflow behavior
  • subclass tests that verify specialized steps

For template-method patterns, test that base orchestration calls extension points in the expected order. This protects behavior when subclasses are added later. Without these tests, inheritance hierarchies can drift and violate assumptions silently.

Example test idea:

  • instantiate a minimal test subclass
  • capture method call sequence
  • assert shared template method still enforces required order

This approach catches regressions that simple output assertions may miss. It improves long-term maintainability.

Common Pitfalls

  • Creating abstract base classes with little or no shared implementation.
  • Building deep inheritance hierarchies for simple capability sharing.
  • Using abstract classes where composition would isolate change better.
  • Exposing too many protected internals and weakening encapsulation.
  • Mixing unrelated domain concepts into one inheritance tree.

Summary

  • Abstract classes are best for shared behavior plus controlled extension points.
  • Interfaces are better for pure contracts and loose coupling.
  • Composition is often better when behavior changes frequently.
  • Keep inheritance shallow and purposeful.
  • Choose abstraction style based on change patterns, not habit.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.