Skip to content

require-port-for-service

Consider a consumer-owned port when visible service structure suggests a substitution boundary.

Why

A small port can decouple consumers when they genuinely need to substitute a concrete service boundary.

Fix

When a real consumer needs substitution, define a focused consumer-owned Protocol and type that consumer against it; otherwise suppress the advisory instead of adding an unused abstraction.

Examples

Before — flagged Concrete service directly exposes an injected collaborator
app/services/thing_service.py
class ThingService:
def __init__(self, client: ThingClient) -> None:
self.client = client
def read(self, key: str) -> str:
return self.client.get(key)
def write(self, key: str, value: str) -> None:
self.client.put(key, value)
def sync(service: ThingService) -> None:
service.write("inbox", service.read("outbox"))
After — preferred A visible structural port already describes the service boundary
app/services/thing_service.py
from typing import Protocol
class ThingServicePort(Protocol):
def read(self, key: str) -> str: ...
def write(self, key: str, value: str) -> None: ...
class ThingService:
def __init__(self, client: ThingClient) -> None:
self.client = client
def read(self, key: str) -> str:
return self.client.get(key)
def write(self, key: str, value: str) -> None:
self.client.put(key, value)
def sync(service: ThingServicePort) -> None:
service.write("inbox", service.read("outbox"))