CMP-220 · Lesson 7 of 12 · 21 min

Abstract classes and interfaces

Vehicle classification is where the Hub earns its money. A city wants five simple classes for its planning studies; the provincial ministry wants an axle-based scheme with a dozen classes for pavement design; a toll study wants something else again. Tally.java handled this with a method called classify that had grown to 400 lines of nested conditions and a parameter called mode. Dana wants classification to be pluggable: the Hub should accept any classifier that obeys a common contract, and a customer's contract should name the classifier to use. The tools for writing contracts in object-oriented languages are abstract classes and interfaces, and this lesson covers both, when to choose each, and how Python and C++ express the same ideas.

Abstract methods and abstract classes

Look back at CounterNode from lesson 5. Every real node is a LoopNode, RadarNode or CameraNode; nobody should ever create a plain CounterNode, which corresponds to no product. Java lets you say so by declaring the class abstract. An abstract class cannot be instantiated with new; it exists to be extended. It may declare abstract methods, which have a signature but no body, and every concrete (non-abstract) subclass must override them all, or the compiler refuses to compile it. An abstract class may also have fields, constructors and fully implemented methods, so it can hold the state and code its subclasses share.

An abstract class holds shared state and code and declares abstract methods that each concrete subclass must implement.
public abstract class CounterNode {
    private final int id;
    protected CounterNode(int id) { this.id = id; }   // called only by subclasses
    public int id() { return id; }

    public abstract String model();                  // every subclass must answer
    public abstract boolean measuresSpeed();

    public String describe() {                       // shared code uses the abstract parts
        return model() + " node " + id + (measuresSpeed() ? " (speed)" : "");
    }
}

public final class RadarNode extends CounterNode {
    public RadarNode(int id) { super(id); }
    @Override public String model() { return "NS-4"; }
    @Override public boolean measuresSpeed() { return true; }
}

The template method pattern

The describe method above is a small example of a common design: an abstract class defines the skeleton of an algorithm in a concrete method, and leaves certain steps abstract for subclasses to fill in. This is the template method pattern. The Hub's report generators use it. Every report follows the same sequence: load the counts for a period, check them for gaps, format a header, format each row, format a footer, and write the result. Only the formatting differs between a CSV file for engineers, a PDF summary for council and a JSON feed for the city's open-data portal. The abstract class ReportGenerator implements generate() once, as a final method so that no subclass can change the sequence, and declares header, row and footer abstract. A new format means writing three small methods, and the gap checking, which is the subtle part, is written and tested once.

Interfaces

An interface is a pure contract: a named set of method signatures that a class promises to provide. A class implements an interface with the keyword implements and must then supply a body for every abstract method in it. Unlike superclasses, a class may implement any number of interfaces, so interfaces let one class play several unrelated roles. CameraNode can be a CounterNode by inheritance and also implement Reaimable, Upgradable and AutoCloseable. Interfaces were Java's answer to the problems of multiple inheritance in C++: a class inherits implementation from one place only, but can promise many behaviours.

The Classifier interface: abstract methods, a default method and a static factory. Each scheme is a separate class.
public interface Classifier {
    VehicleClass classify(Detection d);              // abstract: no body
    String name();

    default boolean canClassify(Detection d) {      // default method: inherited unless overridden
        return true;
    }
    static Classifier forContract(String scheme) {   // static factory on the interface
        return switch (scheme) {
            case "simple5" -> new LengthClassifier();
            case "axle13"  -> new AxleClassifier();
            default -> throw new IllegalArgumentException("unknown scheme " + scheme);
        };
    }
}

public final class LengthClassifier implements Classifier {
    @Override public VehicleClass classify(Detection d) {
        int len = d.lengthCm();
        if (len < 280)  return VehicleClass.MOTORCYCLE;
        if (len < 600)  return VehicleClass.CAR;
        if (len < 900)  return VehicleClass.LIGHT_TRUCK;
        if (len < 1400) return VehicleClass.BUS;
        return VehicleClass.HEAVY_TRUCK;
    }
    @Override public String name() { return "simple5"; }
}

public final class AxleClassifier implements Classifier {
    @Override public boolean canClassify(Detection d) { return d.hasAxles(); }
    @Override public VehicleClass classify(Detection d) { /* axle count and spacing rules */ ... }
    @Override public String name() { return "axle13"; }
}

Since Java 8, interfaces may also contain default methods, which have bodies and are inherited by implementing classes unless they override them, and static methods, which belong to the interface itself. Default methods were added so that interfaces in Java's own library, such as List and Map, could gain new methods without breaking every class that already implemented them. They are useful for small conveniences built on the abstract methods, like canClassify above, but an interface still cannot hold instance fields, so it cannot hold per-object state.

A UML class diagram. A box marked with the stereotype interface and named Classifier lists classify and name in italics and canClassify as a default method. Three classes, LengthClassifier, AxleClassifier and CameraClassifier, connect to it with dashed lines ending in hollow triangles at the interface. To the left, a ClassificationService box has a solid arrow to Classifier labelled uses. Separately, an abstract class ReportGenerator, with its name in italics, has a final generate method and abstract header, row and footer methods, and CsvReport, PdfReport and JsonReport connect to it with solid lines and hollow triangles.
Interface realization (dashed line, hollow triangle) and abstract class extension (solid line, hollow triangle) in UML. Abstract names and methods are written in italics.

In UML an interface is drawn as a class box with the stereotype «interface» above its name, and a class that implements it is joined by a dashed line with a hollow triangle, called realization. Abstract classes and abstract methods are written in italics. The diagram shows the two designs side by side: ClassificationService depends only on the Classifier interface, and ReportGenerator shares an algorithm with its subclasses.

Programming to an interface

The most important habit this lesson teaches is to declare variables, parameters, fields and return types with interface types whenever you can: Classifier c, not LengthClassifier c; List<Detection> batch, not ArrayList<Detection> batch. Code written against an interface works with every implementation, including ones written later, so the decision about which implementation to use is made in one place, often at start-up from configuration, and everything else is shielded from it. The ClassificationService that handles uploads never names a concrete classifier; it receives one in its constructor, by dependency injection from lesson 4, and calls classify. Adding the camera-based scheme for the NS-5 required one new class and one line of configuration.

Interfaces also define capabilities. Lesson 5 left a problem unsolved: temporary nodes must not be re-aimed, but RadarNode promised that reaim works. The fix is a small interface, Reaimable, with one method, implemented only by node classes that support it. The operator console takes a List<Reaimable>, so a temporary node can never be passed to it; the compiler enforces what the old exception only reported at run time. Small, focused interfaces like this are the subject of the interface segregation principle in lesson 10.

A contract is more than signatures

An interface's method signatures are checked by the compiler, but they say nothing about behaviour. Two classes can both implement classify(Detection) and still disagree about what happens with a detection that has no length, whether the method may return null, or whether it is safe to call from several threads at once. The rest of the contract lives in documentation: preconditions that callers must meet, postconditions the method guarantees, the exceptions it may throw and its thread-safety. In Java this is written in Javadoc comments on the interface, which every implementer must honour; the Liskov substitution principle from lesson 5 applies to implementations of interfaces exactly as it does to subclasses. The Classifier documentation states that classify is called only when canClassify returned true, that it never returns null, that it must not change the detection, and that implementations must be safe to share between threads because the Hub classifies uploads in parallel. A classifier that kept a mutable counter in a field would compile and pass a single-threaded test, yet break that last promise in production.

Functional interfaces and lambdas

An interface with exactly one abstract method is a functional interface, and Java lets you supply an implementation with a lambda expression instead of a whole class. Classifier has two abstract methods, so it does not qualify, but a simpler Predicate<Detection>, which takes a detection and returns a boolean, does. The Hub's data-quality filters are written this way: Predicate<Detection> plausibleSpeed = d -> !d.hasSpeed() || d.speedKmh() <= 160; and filters can be combined with and, or and negate. Lambdas are object-oriented underneath: the compiler creates an object that implements the interface. They make the strategy pattern of lesson 11 almost effortless.

Abstract class or interface?

Choosing between an abstract class and an interface in Java.
QuestionAbstract classInterface
How many can a class have?One (single inheritance)Any number
Instance fields (state)?YesNo (constants only)
Constructors?Yes, called by subclassesNo
Method bodies?Yes, anyDefault and static methods only
Typical useShared state and code in a family of closely related classesA role or capability that unrelated classes can provide
Example in the HubCounterNode, ReportGeneratorClassifier, Reaimable, AutoCloseable

A common and effective combination is to publish an interface as the contract and provide an abstract class that implements part of it as a convenience for implementers, a skeletal implementation. Java's collections do exactly this: List is the interface and AbstractList implements most of it in terms of get and size, so writing a new list type takes only a few methods. Callers depend on List; implementers may extend AbstractList or not, as they wish.

The same ideas in Python and C++

Python's abc module provides abstract base classes: a class that inherits from ABC and marks methods with @abstractmethod cannot be instantiated until a subclass overrides them all. Python 3.8 added typing.Protocol, which declares an interface structurally: any class with the right methods satisfies the protocol without inheriting from it, which formalizes duck typing for static checkers such as mypy. C++ has no interface keyword; an abstract class is any class with at least one pure virtual function, declared virtual R f() = 0;, and a class whose members are all pure virtual functions plus a virtual destructor serves as an interface.

from abc import ABC, abstractmethod
from typing import Protocol

class Classifier(ABC):
    @abstractmethod
    def classify(self, d): ...
    def can_classify(self, d):           # a concrete method shared by all classifiers
        return True

class LengthClassifier(Classifier):
    BANDS = [(280, "motorcycle"), (600, "car"), (900, "light truck"), (1400, "bus")]
    def classify(self, d):
        for limit, cls in self.BANDS:
            if d["length_cm"] < limit:
                return cls
        return "heavy truck"

class AxleClassifier(Classifier):
    def can_classify(self, d):
        return "axles" in d
    def classify(self, d):
        return {2: "class 2-3", 3: "class 6", 5: "class 9"}.get(d["axles"], "other")

try:
    Classifier()
except TypeError as e:
    print("cannot instantiate:", str(e).split(" with")[0])

class Formatter(Protocol):               # structural interface: no inheritance needed
    def row(self, cls: str, n: int) -> str: ...

class CsvFormatter:
    def row(self, cls, n): return f"{cls},{n}"

def report(batch, clf: Classifier, fmt: Formatter):
    counts = {}
    for d in batch:
        if clf.can_classify(d):
            c = clf.classify(d)
            counts[c] = counts.get(c, 0) + 1
    return [fmt.row(c, n) for c, n in sorted(counts.items())]

batch = [{"length_cm": 455}, {"length_cm": 1720, "axles": 5}, {"length_cm": 230},
         {"length_cm": 470, "axles": 2}, {"length_cm": 1180}]
for clf in (LengthClassifier(), AxleClassifier()):
    print(type(clf).__name__, report(batch, clf, CsvFormatter()))
Output
cannot instantiate: Can't instantiate abstract class Classifier
LengthClassifier ['bus,1', 'car,2', 'heavy truck,1', 'motorcycle,1']
AxleClassifier ['class 2-3,1', 'class 9,1']

Worked problem

Designing the contract for camera classification

The NS-5 classifies vehicles on the node, sends a class code with a confidence between 0 and 1, and sometimes sends no class when the camera is blinded by sun glare. The Hub must use the camera's class when confidence is at least 0.8 and fall back to length-based classification otherwise. Design CameraClassifier so that ClassificationService needs no changes.

Show solution
  1. ClassificationService depends only on Classifier, so CameraClassifier must implement Classifier, including classify and name.
  2. Handle the fallback inside the new class by composition: CameraClassifier holds a LengthClassifier, received in its constructor, and delegates to it when the camera's confidence is below 0.8 or missing.
  3. Override canClassify to return true when either the camera result or a length is present, so the service skips only detections that have neither.
  4. Register the scheme name camera5 in the factory or the configuration that builds classifiers.

Answer: CameraClassifier implements Classifier and wraps a fallback classifier. The service and every report remain unchanged, because they were written to the interface.

Common mistakes

  • MistakeDeclaring variables with concrete types such as ArrayList or LengthClassifier.

    InsteadUse interface types such as List and Classifier so implementations can change in one place.

  • MistakeUsing an abstract class when unrelated classes need the same role.

    InsteadUse an interface; a class can implement many interfaces but extend only one class.

  • MistakePutting large amounts of logic in default methods.

    InsteadKeep default methods small conveniences; real behaviour and state belong in classes.

  • MistakeCreating huge interfaces with many methods.

    InsteadPrefer small, focused interfaces so implementers are not forced to provide methods they cannot support.

  • MistakeMaking the template method overridable.

    InsteadDeclare the skeleton method final so subclasses can change the steps but not the sequence.

Summary and key terms

  • An abstract class cannot be instantiated, may hold state and code, and declares abstract methods that concrete subclasses must implement.
  • The template method pattern fixes an algorithm's skeleton in a final method and leaves steps to subclasses.
  • An interface is a contract of method signatures; a class can implement many, and default and static methods add conveniences.
  • Program to interfaces: declare types as interfaces and choose implementations in one place.
  • Python offers abstract base classes and structural Protocols; C++ uses classes with pure virtual functions.
Abstract class
A class that cannot be instantiated and may declare abstract methods for subclasses to implement.
Abstract method
A method declared without a body that concrete subclasses must override.
Interface
A named contract of method signatures that classes promise to implement.
Default method
An interface method with a body that implementing classes inherit unless they override it.
Template method
A design in which a superclass method defines an algorithm's steps and subclasses supply some of them.
Functional interface
An interface with exactly one abstract method, which can be implemented with a lambda.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Can an abstract class have a constructor? Why would it?

    Show answer

    Yes; subclasses call it with super(...) to initialize the shared fields and check shared invariants.

  2. A class must be a CounterNode, Reaimable and AutoCloseable. How is that expressed in Java?

    Show answer

    class X extends CounterNode implements Reaimable, AutoCloseable.

  3. Why should ReportGenerator.generate be final?

    Show answer

    So no subclass can change the sequence of steps, such as skipping the gap check.

  4. Classify a 1,250 cm vehicle with the length bands in this lesson.

    Show answer

    Bus, since 900 ≤ 1,250 < 1,400.

  5. What is the C++ equivalent of a Java abstract method?

    Show answer

    A pure virtual function, declared virtual R f() = 0;.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz