Challenge

Add a method to a published interface

20 minintermediate210 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • Adding an abstract method to a published interface breaks every implementor
  • A default method is source- and binary-compatible, which is why it exists
  • A good default derives from the abstract methods and needs no state
  • When no sensible default exists, that is information about the design

Starter

Starter.javaOpen in playground
import java.util.*;

/**
 * CHALLENGE — 20 minutes.
 *
 * You own Repository. Three teams implement it, and you cannot edit their
 * code — treat the three classes below as if they were in other repositories
 * that you do not control.
 *
 * Product wants `exists(id)`.
 *
 * TASKS
 *   1. Add `boolean exists(ID id);` as an ABSTRACT method. Compile. Count
 *      the errors. That count is the problem default methods were built for.
 *   2. Make it a default method instead, expressed in terms of the
 *      interface's existing abstract methods. All three must compile with no
 *      edits.
 *   3. Product also wants `countAll()`. Write down whether a default is a
 *      good idea here, and why it is a different question from exists().
 *   4. Pretend you own InMemoryRepository after all, and override exists()
 *      there because it can do better. Say in a comment why overriding a
 *      default is the system working, not a smell.
 */
public class Starter {

    record Order(String id, int paise) {}

    /** The published contract. You own this file and only this file. */
    interface Repository<T, ID> {
        Optional<T> findById(ID id);

        List<T> findAll();

        // TASK 1: add exists(ID) here, abstract first, then as a default.
    }

    /* ── three implementors you do not control ── */

    static final class InMemoryRepository implements Repository<Order, String> {
        private final Map<String, Order> rows = new LinkedHashMap<>();

        void save(Order order) {
            rows.put(order.id(), order);
        }

        @Override public Optional<Order> findById(String id) {
            return Optional.ofNullable(rows.get(id));
        }

        @Override public List<Order> findAll() {
            return List.copyOf(rows.values());
        }
    }

    static final class ReadOnlyRepository implements Repository<Order, String> {
        private final List<Order> rows = List.of(new Order("ORD-9", 4200));

        @Override public Optional<Order> findById(String id) {
            return rows.stream().filter(o -> o.id().equals(id)).findFirst();
        }

        @Override public List<Order> findAll() {
            return rows;
        }
    }

    /** Pretend this one talks to a database and every call is expensive. */
    static final class JdbcRepository implements Repository<Order, String> {
        private int queries = 0;

        int queries() {
            return queries;
        }

        @Override public Optional<Order> findById(String id) {
            queries++;
            return id.equals("ORD-1") ? Optional.of(new Order("ORD-1", 1000)) : Optional.empty();
        }

        @Override public List<Order> findAll() {
            queries++;
            return List.of(new Order("ORD-1", 1000));
        }
    }

    public static void main(String[] args) {
        InMemoryRepository memory = new InMemoryRepository();
        memory.save(new Order("ORD-1", 1000));

        List<Repository<Order, String>> all =
                List.of(memory, new ReadOnlyRepository(), new JdbcRepository());

        for (Repository<Order, String> repo : all) {
            System.out.printf("%-22s findAll=%d%n",
                    repo.getClass().getSimpleName(), repo.findAll().size());
            // Once exists() is added:
            // System.out.printf("  exists(ORD-1) = %s%n", repo.exists("ORD-1"));
        }
    }
}

Run it locally:

cd exercises/java/oop/abstract-class-vs-interface/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Add it as abstract first and count the compile errors. That number is what default methods were invented to make zero.

  2. Hint 2

    exists(id) can be answered entirely in terms of findById. That is what makes it a safe default.

  3. Hint 3

    Now try countAll(). Can it be derived from the existing methods without being wrong or slow for somebody?

  4. Hint 4

    If a default would be wrong for one implementor, is the method on the right interface at all?

Done when

  • exists(id) is added and all three implementors compile untouched
  • The default is expressed in terms of the interface's own methods
  • A comment explains why a default for countAll() is a worse idea
  • One implementor overrides the default, with a comment saying why it can do better

Stretch

InMemoryRepository can answer exists() far more cheaply than findById() does, and JdbcRepository can answer it with SELECT 1. Override it in both and then argue the general point: a default method is a correctness guarantee, not a performance one, and an implementor overriding it is the system working as designed rather than a smell.

← Back to When do you choose an abstract class over an interface?