Oop

Abstraction & Interfaces

Abstraction as a design tool: depend on what something does, not how it does it. An interface is a contract that lets a service swap email for SMS, or for a test fake, without changing a line; an abstract class fixes the steps and lets subclasses fill in the parts that vary (the template method). When to choose which, why static state and static calls make code hard to change and test, and where nested classes fit. The Java language rules live in the Java guide; every example here runs in Java and Python, with verified output.

Suggest an edit

Abstraction & Interfaces — Depending on What, Not How

An order service must tell customers when their order is confirmed. Today it does that by email. Next quarter it must also send an SMS, and in tests it should send nothing at all. If the service creates an EmailSender itself, each of those changes means editing the service. If it depends only on "something that can send a notification", none of them do.

That is abstraction used as a design tool: give a name to what another object does, hide how it does it, and let the rest of the system depend only on the name. This lesson covers the two ways to express that, interfaces and abstract classes, and the costs of static. The Java language rules (declaring interfaces, abstract, default methods, nested classes) are in the Java guide's Abstract Classes & Interfaces, static vs Instance and Nested & Anonymous Classes; Lambdas.

💡 The core idea.

  • An interface is a contract: it lists what an object can do. Code that depends on an interface works with any class that implements it, including a fake one used in tests.
  • An abstract class holds the structure its subclasses share, and lets each subclass fill in the parts that differ.
  • static fields and calls to static methods are dependencies fixed in the code: easy to write, but hard to replace or test.

This builds on Encapsulation, Inheritance & Polymorphism. Every output below was produced by running the code on Java 21 and Python 3.11.

You'll be able to: design a class that depends on an interface, and switch to a different implementation (including a test fake) without changing the class; choose between an interface and an abstract class; recognise when static state or a static call is a hidden dependency.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the design does to the code that uses it.
  2. A concrete bite — a specific situation where the design choice pays off or hurts.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Interfaces: program to a contract
  2. Abstract classes: shared steps, varying parts
  3. Interface or abstract class?
  4. static and nested classes in design
  5. Mental-model summary
  6. Gotcha checklist
  7. Check yourself
  8. Sources

1. Interfaces: program to a contract

Here the order service depends on a Notifier interface, and receives an object that implements it through its constructor:

Output (Java; Python prints the list with quotes):

EMAIL to [email protected]: order #42 confirmed
SMS to +44 7700 900123: order #43 confirmed
test saw: [test-user|order #44 confirmed]

Analysis. The same OrderService, unchanged, sent an email, then an SMS, and the third time sent nothing at all. The RecordingNotifier only stored the message, so a test can check exactly what would have been sent, without needing a mail server. In Python, a Protocol describes the contract, and any class with a matching send method satisfies it, without having to inherit from it [4].

Intuition. Mechanism. OrderService mentions only the interface. Which class actually sends the message is decided by the code that creates the service. That is called dependency injection, and it is covered in Dependency Injection & Error Handling. It is also the D of SOLID: depend on abstractions, not on concrete classes. "Program to an interface, not an implementation" is one of the oldest rules of object-oriented design [1].

Concrete bite. If placeOrder contained new EmailNotifier(), every test of placing an order would send a real email, or would need a mocking library to intercept it. Adding SMS would mean editing OrderService, a class whose job is orders, not messaging.

💡 Earned rule. When a class uses something that may change, or that is slow, external or awkward to use in tests (payment gateways, message senders, clocks, storage), make the class depend on an interface, and pass in the implementation from outside.

The cost is one more type and one more constructor parameter. Don't add an interface for a class that has only one implementation and no reason to be replaced. Create the interface when a second implementation, or the first test, needs it.

Your Turn — Practice: Interfaces

One PaymentGateway contract, two implementations. The driver processes each payment through the interface type without caring which concrete class it is — that's the point of programming to an interface.


2. Abstract classes: shared steps, varying parts

Sometimes several classes follow the same fixed procedure and differ only in a few steps. An abstract class can hold the shared procedure, and declare the differing steps abstract, for each subclass to fill in:

Output:

== Sales, March ==
orders: 1,204  revenue: 38,900
-- generated by ReportService --
== Inventory ==
SKUs low on stock: 7
-- generated by ReportService --

Analysis. Both reports have the same header and footer, and neither subclass repeats that code. render() is the fixed procedure (Java's final keyword stops subclasses from changing it), and title() and body() are the steps that differ. This shape is the Template Method pattern, covered with the other behavioural patterns.

Intuition. Mechanism. An abstract class can have fields, constructors and fully written methods; an interface cannot have instance fields or constructors. You cannot create an object of an abstract class directly, and a class can extend only one class [2].

Concrete bite. That one-parent limit is the cost. A SalesReport that extends Report cannot also extend any other class. Interfaces have no such limit: a class can implement as many as it needs.

Your Turn — Practice: Abstraction

Define an abstract Animal with an abstract makeSound(), then let Dog and Cat supply their own — runtime polymorphism through an abstract base.


3. Interface or abstract class?

Question Interface Abstract class
What does it describe? an ability or role: can send, can be priced a family of classes with a shared structure: is a report
Instance fields and constructors no yes
Shared method code default methods, which can't use fields yes, and it can use the class's fields
How many a class can have many one
Swapping in a test fake easy: implement the interface harder: must extend the class

💡 Earned rule. Use an interface by default for anything another class depends on. If several implementations share real code or fields, add an abstract class behind the interface, so that other code still depends only on the interface.

The cost of using both is two types where one might be enough. In return, other code depends on the flexible contract, while the implementations still share code.


4. static and nested classes in design

static members belong to the class, not to any one object: there is a single copy, and any code can reach it. The Java guide's static vs Instance covers the rules. In design, static has two very different uses:

  • Static methods that use no stored state are fine: Math.max, a Money.parse(String) that only reads its argument, or a static factory method such as List.of(...), whose name says how the object is made.
  • Static fields that change, and static calls to things that can change, are hidden dependencies. A static Config current field, or a call to a static PaymentGateway.charge(...) inside a method, cannot be replaced with a test fake. It is shared by every test and every thread. And it doesn't appear in any constructor, so someone reading the class can't see that the class depends on it.

The usual fix is the one from section 1: turn the static dependency into an interface, and pass an implementation in. A clock is the classic example. Code that calls LocalDate.now() directly can't be tested as if it were a leap day; code that is given a java.time.Clock can [3].

Nested classes are classes declared inside another class. In design they have one purpose: keeping a helper class next to the only class that uses it. A static nested class (such as a Builder inside the class it builds, or a Node inside a linked list) doesn't need an object of the outer class. An inner (non-static) class keeps a hidden reference to an outer object. Prefer static nested classes, unless the helper really needs the outer object's fields. The kinds and their rules are in the Java guide's Nested & Anonymous Classes; Lambdas.

💡 Earned rule. Use static only for constants, methods that don't use stored state, and factory methods. Anything that holds changing state, or reaches the outside world (the time, random numbers, the network, storage), should be an object that is passed in.

The cost is a few more constructor parameters. The benefit is code whose dependencies are visible, and can be replaced.


5. Mental-model summary

Principle Consequence
An interface is a contract; other code depends only on it Implementations, including test fakes, can be switched without changing that code
Pass implementations in from outside (dependency injection) The class that uses another object doesn't choose which implementation it gets
An abstract class holds shared steps and fields Subclasses fill in the parts that differ (Template Method); a class has only one parent
Use interfaces for dependencies by default Add an abstract class behind the interface to share code
Changing static fields and static calls are hidden dependencies Hard to test and shared by everything; pass an object in instead
Static nested classes keep helper classes close to their user Prefer them over inner classes, which hold a reference to the outer object

6. Gotcha checklist

Symptom Likely cause Fix
Tests send real emails or hit real services the class creates its collaborators with new depend on an interface; inject the implementation
Adding a new channel means editing the service the service names a concrete class program to an interface
Tests pass alone and fail together shared static mutable state make it an instance field of an injected object
Date-dependent code can't be tested direct calls to now() inject a Clock
A class needs two abstract parents abstract classes allow only one make the roles interfaces; share code by composition
An interface with one implementation and no tests needing a fake an abstraction added "just in case" inline it until a second implementation exists

✅ Check yourself

One check per objective. Answer before you open anything.

A CheckoutService calls StripeClient.charge(...), a static method, directly. How would you change the design so you can add a second payment provider and test checkout without charging cards?

Define a PaymentGateway interface with a charge method. Write a StripeGateway implements PaymentGateway class whose charge makes the static call, and a second implementation for the other provider. Give CheckoutService a PaymentGateway in its constructor. Tests pass a fake gateway that records charges and can be told to fail, so both the success and failure paths can be tested.


📚 Sources

  1. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994), ch. 1.
  2. The Java Language Specification, Java SE 21, §8.1.1.1 "abstract Classes" — https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html#jls-8.1.1.1
  3. java.time.Clock, Java SE 21 API — https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Clock.html
  4. Python 3 documentation, typing.Protocol and abc — https://docs.python.org/3/library/typing.html#typing.Protocol

🧪 Predict, then check.

  1. In section 1, write a PushNotifier and pass it to OrderService. Predict which lines of OrderService you had to change.
  2. In section 2, try to override render() in SalesReport. Predict what Java says.
  3. In the Python version from section 2, delete body() from InventoryReport. Predict what happens when it is created.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionff97b7f