A logbook for curious waters
← Software Engineering Field Guide

Extract Interface

Before you begin: Classes, methods, and constructor parameters.

Extract Interface gives a caller’s required operations a separate name and contract. The caller can then depend on that capability rather than on one concrete class. An implementation still has to supply the actual behavior. 1

Original version: the reader asks for a particular storage class

public sealed class MemoryText
{
    private readonly string text;
    public MemoryText(string text) { this.text = text; }
    public string Read() { return text; }
}
public sealed class Reader
{
    private readonly MemoryText source;
    public Reader(MemoryText source) { this.source = source; }
    public string Show() { return source.Read(); }
}
class MemoryText:
    def __init__(self, text):
        self._text = text

    def read(self):
        return self._text


class Reader:
    def __init__(self, source: MemoryText):
        self._source = source

    def show(self):
        return self._source.read()

The reader needs text, not necessarily the particular in-memory implementation. Python’s annotation documents a concrete type but does not enforce it at runtime.

Refactored version: name the text-reading capability

public interface ITextSource
{
    string Read();
}
public sealed class MemoryText : ITextSource
{
    private readonly string text;
    public MemoryText(string text) { this.text = text; }
    public string Read() { return text; }
}
public sealed class EmptyText : ITextSource
{
    public string Read() { return ""; }
}
public sealed class Reader
{
    private readonly ITextSource source;
    public Reader(ITextSource source) { this.source = source; }
    public string Show() { return source.Read(); }
}

The reader accepts ITextSource. Both shown implementations honor the promise to return a string, including the empty-string case.

from typing import Protocol


class TextSource(Protocol):
    def read(self) -> str:
        ...


class MemoryText:
    def __init__(self, text):
        self._text = text

    def read(self) -> str:
        return self._text


class EmptyText:
    def read(self) -> str:
        return ""


class Reader:
    def __init__(self, source: TextSource):
        self._source = source

    def show(self):
        return self._source.read()

The protocol documents the expected shape for readers and static tools. Implementations need not inherit from it to be compatible. Runtime calls still rely on the supplied object providing the method.

Extract a contract, not a photocopy

An interface need not reproduce every public method of the original class. Include what the client needs, and describe relevant semantics: missing data, errors, ownership, and whether repeated calls can differ.

The old in-memory result remains unchanged; the empty implementation demonstrates substitution, not a requirement to create several classes for every interface. A single implementation can justify an interface when it protects a real boundary or supports controlled tests. Conversely, an interface with no useful boundary can add needless navigation.

Sources

  1. Martin Fowler — Changes for the second edition of Refactoring, documenting Extract Interface in the original catalog.
  2. Python — Protocols and Microsoft — Interfaces.