Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deep Modules

From “A Philosophy of Software Design”:

Deep module = small interface + lots of implementation

┌─────────────────────┐
│   Small Interface   │  ← Few methods, simple params
├─────────────────────┤
│                     │
│                     │
│  Deep Implementation│  ← Complex logic hidden
│                     │
│                     │
└─────────────────────┘

Shallow module = large interface + little implementation (avoid)

┌─────────────────────────────────┐
│       Large Interface           │  ← Many methods, complex params
├─────────────────────────────────┤
│  Thin Implementation            │  ← Just passes through
└─────────────────────────────────┘

When designing interfaces, ask:

  • Can I reduce the number of methods?
  • Can I simplify the parameters?
  • Can I hide more complexity inside?

Python: Interfaces and Privacy

Python has no interface keyword and no access modifiers. We use two conventions to enforce deep module design:

ABC as interface. Define the public surface of a module as an ABC with @abstractmethod. Callers depend on the ABC, not the concrete class. This is how Ports are defined.

from abc import ABC, abstractmethod

class JokesRepositoryPort(ABC):
    """Public interface: two methods, simple params."""

    @abstractmethod
    def get_jokes(self) -> list[Joke]: ...

    @abstractmethod
    def create_joke(self, joke: Joke) -> Joke: ...

_underscore as private. Everything that is not part of the public interface gets a _ prefix. Ruff and basedpyright treat _ names as internal and warn on external access. Keep the complex logic in _ helpers; keep the public surface small.

class FileStorageRepository(JokesRepositoryPort):
    """Deep implementation: rich logic hidden behind _ helpers."""

    def get_jokes(self) -> list[Joke]:
        return [self._read_joke(p) for p in self._storage_dir.glob("*.json")]

    def create_joke(self, joke: Joke) -> Joke:
        self._write_joke(joke)
        return joke

    def _read_joke(self, path: Path) -> Joke:
        ...  # file I/O, parsing, error handling

    def _write_joke(self, joke: Joke) -> None:
        ...  # serialisation, atomic write

The public interface is two methods. All the complexity is hidden. Tests only call get_jokes and create_joke.

Module Structure

In this project, a module is a directory with an __init__.py. Modules are one level deep — no modules inside modules.

src/
  issuer_agent/       ← module
    __init__.py
    ssi_agent_adapter.py
  metadata/           ← module
    __init__.py
    metadata_repository.py
    postgresql_adapter.py

Each module exposes its public API through __init__.py. Internal files within a module are implementation details — they may use _ prefixed names freely. Do not create sub-modules (a directory inside a module directory).