0

I — Interface Segregation Principle: many specific over one fat interface

Intermediate5 min read·eng-01-008
interviewsolidcompare

Concept

The Interface Segregation Principle states: clients should not be forced to depend on interfaces they do not use. In other words, prefer many small, specific interfaces over one large, general-purpose interface.

Fat interfaces are the ISP equivalent of fat classes. When you create an interface with 15 methods, every class that implements it must provide all 15 methods — even if it only meaningfully supports 3 of them. The other 12 become empty stubs or exception-throwing placeholders, which is both an ISP violation and an LSP violation at the same time.

The motivation behind ISP is that callers should only depend on the capabilities they actually need. If InvoiceGenerator only needs to read from a repository, it should not depend on an interface that also includes create(), update(), and delete(). Depending on the full interface creates unnecessary coupling — a change to the write methods of the repository could force InvoiceGenerator to be recompiled or re-tested even though it never called those methods.

The practical design rule: design interfaces from the caller's perspective, not the implementor's. Ask: "What does this specific caller need?" Design the interface to answer that question exactly. Multiple callers may use the same underlying implementation, each through a different narrow interface.

Fat InterfaceSegregated Interfaces
Cacheable with get, set, delete, flush, getMultiple, setMultiple, deleteMultipleCacheReader (get, getMultiple), CacheWriter (set, setMultiple), CacheInvalidator (delete, flush)
Storage with read, write, delete, list, move, copy, urlReadable, Writable, Deletable — implement what you need
Repository with findAll, findById, create, update, delete, paginateReadRepository + WriteRepository — read models use ReadRepository only

Code Example

php
<?php
declare(strict_types=1);

// VIOLATION: Fat interface forces implementors to support things they don't
interface UserRepository
{
    public function findById(int $id): User;
    public function findByEmail(string $email): ?User;
    public function findAll(): array;
    public function create(array $data): User;
    public function update(int $id, array $data): User;
    public function delete(int $id): void;
    public function paginate(int $perPage): LengthAwarePaginator;
    public function exportToCsv(): string;  // ← What?! Persistence concern mixed in
    public function sendPasswordReset(int $id): void; // ← Notification concern!
}

// ReadOnlyReportingRepository is forced to implement create/update/delete/sendPasswordReset
class ReadOnlyReportingRepository implements UserRepository
{
    public function findById(int $id): User { /* real impl */ }
    public function findAll(): array { /* real impl */ }

    // VIOLATION: forced to implement methods that make no sense here
    public function create(array $data): User {
        throw new \BadMethodCallException('Not supported');
    }
    public function update(int $id, array $data): User {
        throw new \BadMethodCallException('Not supported');
    }
    public function delete(int $id): void {
        throw new \BadMethodCallException('Not supported');
    }
    // ... 4 more stubs
}

// CORRECT: Segregated interfaces — each caller gets exactly what it needs

interface UserReader
{
    public function findById(int $id): User;
    public function findByEmail(string $email): ?User;
    public function findAll(): array;
    public function paginate(int $perPage): LengthAwarePaginator;
}

interface UserWriter
{
    public function create(array $data): User;
    public function update(int $id, array $data): User;
}

interface UserDeleter
{
    public function delete(int $id): void;
}

// Full implementation supports all interfaces
class EloquentUserRepository implements UserReader, UserWriter, UserDeleter
{
    public function findById(int $id): User
    {
        return User::findOrFail($id);
    }

    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }

    public function findAll(): array
    {
        return User::all()->all();
    }

    public function paginate(int $perPage): LengthAwarePaginator
    {
        return User::paginate($perPage);
    }

    public function create(array $data): User
    {
        return User::create($data);
    }

    public function update(int $id, array $data): User
    {
        $user = $this->findById($id);
        $user->update($data);
        return $user->fresh();
    }

    public function delete(int $id): void
    {
        $this->findById($id)->delete();
    }
}

// Read-only report class depends only on what it uses
final class UserActivityReport
{
    public function __construct(private readonly UserReader $users) {}

    public function generate(): array
    {
        return array_map(
            fn(User $u) => [
                'name'       => $u->name,
                'last_login' => $u->last_login_at,
            ],
            $this->users->findAll()
        );
    }
}

// Registration action only needs write access
final class RegisterUserAction
{
    public function __construct(private readonly UserWriter $users) {}

    public function execute(array $data): User
    {
        return $this->users->create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }
}

Interview Q&A

Q: How do you know when an interface is too fat?

Three signals: (1) Implementors throw BadMethodCallException or return null from methods they were forced to implement — this is ISP and LSP violated simultaneously. (2) You find yourself writing "this class only needs methods A and B but must implement A through G." (3) A change to an interface method causes 10 different classes to need updates, most of which do not meaningfully use that method. When you see any of these, split the interface along the lines of what different callers actually need.


Q: Does ISP mean every interface should have one method?

No — that extreme leads to interface explosion where every tiny capability is its own interface and nothing coheres. ISP means interfaces should be role-based from the caller's perspective. A CacheStore interface with get, set, and delete is perfectly cohesive — any caller that uses a cache store needs all three. What ISP forbids is bundling unrelated capabilities together: CacheStore should not also have queueJob() or sendMail() just because the underlying class happens to support those. Group by caller role, not by implementor capability.


Q: How does PHP's type system support ISP?

PHP supports multiple interface implementation (implements A, B, C) and interface extension (interface C extends A, B), making ISP easy to apply. A concrete class can implement multiple narrow interfaces and be injected into different callers through the appropriate narrow type. PHP also allows interface intersection types since 8.1 (A&B) — useful when a dependency must fulfill two roles simultaneously. The constructor hint private readonly UserReader&UserWriter $repo expresses "I need something that can both read and write." All of this makes PHP's type system well-suited for the ISP-driven design of role-based interfaces.