The metric: how many files you must open to understand a snippet. An abstraction appears on the third repetition, not the first. An interface with a single implementation costs navigation and buys no flexibility.
Go
// The Go community says it outright: "accept interfaces, return structs" —
// the consumer declares the interface, and only once implementations >= 2
// ANTIPATTERN: an interface with a single implementation — two places to read
type UserService interface {
QueryUser(ctx context.Context, id UserID) (User, error)
}
type userServiceImpl struct{ db *sql.DB }
// NORM: a direct call, one place to read
func (h *Handler) route(ctx context.Context, id UserID) (User, error) {
return h.db.QueryUser(ctx, id)
}Java
// ANTIPATTERN: an interface with exactly one implementation and one caller —
// +1 file, +1 jump while reading, zero substitutability in practice
public interface UserService { User find(UserId id); }
public final class UserServiceImpl implements UserService { ... }
// NORM: call the class directly. The interface arrives with the
// second implementation — or at the I/O boundary, where a test needs it (5)
public final class UserService { public User find(UserId id) { ... } }Rust
// ANTIPATTERN: dyn for a single implementation — an indirect call
// at runtime and a jump across files while reading
struct Handler { store: Box<dyn Store> }
// NORM: a concrete type while there is one implementation. When a second
// appears, a generic: monomorphization gives flexibility at no runtime cost
struct Handler { store: PostgresStore }Python
# ANTIPATTERN: an ABC hierarchy for a single subclass
class Store(ABC):
@abstractmethod
def get(self, key: str) -> bytes | None: ...
class PostgresStore(Store): ... # the only implementation
# NORM: the class directly. When there are two, a Protocol: it needs no
# inheritance and is declared by the consumer, not the implementation (12)
class PostgresStore:
def get(self, key: str) -> bytes | None: ...