The interface segregation principle (ISP) states that no code should be forced to depend on methods it does not use. ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces. ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy.
In simple terms: Keep your interfaces small, specific, and focused. It is much better to have many tiny, specialized interfaces instead of one giant "fat" interface that forces classes to write useless code.
Importance in object-oriented design: Within object-oriented design, interfaces provide layers of abstraction that simplify code and create a barrier preventing coupling to dependencies. A system may become so coupled at multiple levels that it is no longer possible to make a change in one place without necessitating many additional changes. Using an interface or an abstract class can prevent this side effect.
The Real-World Example: Imagine you want to sign up for a local fitness center.
- The Giant Interface (Violates ISP): The gym only offers a single "$500/month Ultimate Platinum VIP Pass." This pass includes access to the weight room, the swimming pool, the golf simulator, the childcare center, and the juice bar.
- The Problem: You just want to lift weights for 30 minutes. However, you are forced to pay for, carry a card for, and sign waivers for the swimming pool and childcare center, even though you don't swim and don't have kids.
- The Segregated Interface (Adheres to ISP): The gym splits its services. You buy a "Weight Room Only Pass." Parents buy a "Childcare Pass." Swimmers buy a "Pool Pass." You only interact with, pay for, and use exactly what you need.
❌ The Wrong Way: The "Fat" Interface (Violates ISP)
Imagine an interface for handling corporate office employees.
interface Employee {
public function codeSoftware();
public function manageTeam();
public function cleanBuilding();
}
If you try to create a Developer class using this interface, you run into a major problem. Developers don't clean buildings or manage teams. You are forced to write useless filler code just to satisfy the PHP compiler.
class Developer implements Employee {
public function codeSoftware() {
echo "Writing clean code...\n";
}
// ❌ Violates ISP! Forced to implement a method they don't use
public function manageTeam() {
return null;
}
// ❌ Violates ISP! Forced to write useless code
public function cleanBuilding() {
throw new Exception("I am a programmer, not a janitor!");
}
}
✅The Right Way: Small, Focused Interfaces (Adheres to ISP)
To fix this, break the giant Employee interface into tiny, atomic pieces based on actual actions.
interface Codeable {
public function codeSoftware();
}
interface Manageable {
public function manageTeam();
}
interface Cleanable {
public function cleanBuilding();
}
Now, your PHP classes can cleanly pick and choose exactly which contracts apply to them. No wasted code, no dummy methods, and no fake exceptions.
// The Developer class only cares about coding
class Developer implements Codeable {
public function codeSoftware() {
echo "Writing clean code...\n";
}
}
// A Tech Lead class can do both coding and managing
class TechLead implements Codeable, Manageable {
public function codeSoftware() { echo "Writing code...\n"; }
public function manageTeam() { echo "Leading the standup meeting...\n"; }
}
// The Janitor class only cares about cleaning
class Janitor implements Cleanable {
public function cleanBuilding() {
echo "Mopping the floors...\n";
}
}
Why this helps you
- Decoupling: Reduces dependencies between classes, making the code more modular and maintainable.
- Flexibility: Allows for more targeted implementations of interfaces.
- Avoids unnecessary dependencies: Clients don't have to depend on methods they don't use.
The main reason 90% of engineers fail to use the Interface Segregation Principle (ISP) is that they confuse a functional code structure with a business role structure. They create interfaces based on nouns (like interface User or interface Order) instead of behaviors (like CanBeSaved or CanBeEmailed).
This creates massive, bloated contracts that force developers to write empty, useless methods just to satisfy the PHP compiler. Here is how to clear up the confusion and make effortless, high-utility design decisions.
The 3 Main Confusions Cleared Up
1. Confusion: "Does ISP mean I can only have one method per interface?"
- Reality: No. An interface can have multiple methods, as long as every single class that implements it needs every single method.
- Example: A Cacheable interface might need both get($key) and set($key, $value). They belong together because you cannot practically use a cache that only lets you read but never write.
2. Confusion: "If I have 50 small interfaces, won't my codebase become a mess?"
- Reality: It actually makes your codebase much cleaner. In PHP, a single class can implement an unlimited number of interfaces (class User implements Authenticatable, Sluggable, Billable). This is highly flexible, modular, and self-documenting.
3. Confusion: "Can't I just use PHP Traits instead of splitting interfaces?"
- Reality: Traits share implementation code (how things work), while interfaces define architectural architectural contracts (what things do). Traits do not solve type-hinting issues. If a controller expects a giant, bloated interface, a Trait won't prevent a developer from calling a method that shouldn't be there.
The Ultimate ISP Smell Test
You are violating ISP if you look at a PHP class and find yourself doing this:
class GuestUser implements FullUserInterface {
public function getPassword() {
// ❌ ISP Violation Smell!
// Guest users don't have passwords, but the interface forces me to write this.
return null;
}
}
If you ever write return null;, return false;, or throw new Exception("Not implemented"); inside a method required by an interface, your interface is too fat.
The 3-Step Decision Matrix (How to Choose)
When defining a new contract or refactoring an existing one in PHP, use this mental map to ensure ISP compliance:
START ISP_Design_Check
IF Interface_Named_After_Noun IS True THEN
EXECUTE Stop_Shift_Focus_To_Verbs_Or_Actions
ELSE
IF Every_Implementing_Class_Uses_All_Methods IS True THEN
EXECUTE Leave_It_Alone_ISP_Compliant
ELSE
EXECUTE Split_Interface_Into_Smaller_Pieces
END IF
END IF
END ISP_Design_Check
- The "Role vs. Header" Rule: Do not bundle administrative methods with operational methods. For instance, do not put a deleteAccount() method into the same interface that handles basic public user profile viewing.
- The "-able" Naming Trick: When creating interfaces, try naming them with adjectives ending in "-able" or "-or" (e.g., Loggable, Renderable, Exportable, SmsSender). This naturally forces you to focus on a single, specific behavior rather than a massive corporate role.
- Refactor with Interface Inheritance: If you legitimately need a large interface for a complex class, you can still respect ISP by composing it out of smaller interfaces using inheritance:
interface SimpleReader { public function read(); }
interface SimpleWriter { public function write(); }
// Advanced classes can implement this, while simple classes can just use Reader or Writer
interface AdvancedStorage extends SimpleReader, SimpleWriter {
public function clearCache();
}
Summary for Decision Making
Interfaces should be designed from the perspective of the client class that calls them, not the concrete class that implements them. If a client only needs to read data, give it an interface that only allows reading.
The Laravel Blueprint: Mastering ISP
Let's look at a common mistake when building a standard E-commerce checkout or inventory system in Laravel, and how to solve it like a framework architect.
❌ The Junior Approach (Violates ISP)
A junior developer creates a single, monolithic interface to handle all types of products in the store's inventory. [7]
namespace App\Contracts;
interface ProductContract
{
public function getPrice(): int;
public function getWeight(): float;
public function getShippingDimensions(): array;
}
This interface works perfectly for a physical product like a Laptop. But look what happens when the developer tries to implement it for a Digital E-Book or a Software License:
namespace App\Services;
use App\Contracts\ProductContract;
class EBookProduct implements ProductContract
{
public function getPrice(): int {
return 999; // ₹999
}
// ❌ ISP VIOLATION! E-books do not have physical weight.
public function getWeight(): float {
return 0.0; // Useless filler value to satisfy PHP compiler
}
// ❌ ISP VIOLATION! E-books do not ship in boxes.
public function getShippingDimensions(): array {
throw new \Exception("Digital products do not have shipping dimensions!");
}
}
The Problem: Your OrderShippingService iterates over all cart items. If it accidentally calls getShippingDimensions() on an EBookProduct, your entire checkout crashes. The interface forced a digital class to inherit physical real-world problems.
The Laravel Mastery Approach (Adheres to ISP)
To build this like the creators of Laravel, we split the behaviors into small, highly specialized contracts.
Step 1: Segregate the Contracts [8]
namespace App\Contracts;
interface Sellable
{
public function getPrice(): int;
}
interface Shippable
{
public function getWeight(): float;
public function getShippingDimensions(): array;
}
Step 2: Implement only what is required
Now, our classes cleanly bind themselves to exactly what they are capable of doing.
namespace App\Models;
use App\Contracts\Sellable;
use App\Contracts\Shippable;
use Illuminate\Database\Eloquent\Model;
// Physical products implement both contracts
class Laptop extends Model implements Sellable, Shippable
{
public function getPrice(): int { return 6500000; }
public function getWeight(): float { return 2.1; }
public function getShippingDimensions(): array { return; }
}
// Digital products ONLY implement Sellable
class EBook extends Model implements Sellable
{
public function getPrice(): int { return 999; }
// No useless weight or dimension code written here!
}
Step 3: Type-Hint accurately in your Services
Your delivery and shipping managers inside Laravel now type-hint the specific behavioral contract, completely isolating them from digital items.
namespace App\Services;
use App\Contracts\Shippable;
class LogisticsService
{
// This method strictly accepts items that CAN actually be shipped
public function calculateShippingCost(Shippable $product)
{
$weight = $product->getWeight();
$dimensions = $product->getShippingDimensions();
// Calculate carrier rates safely...
}
}
Laravel's Native Mastery Example: Responsable
Laravel natively uses this exact concept with its Illuminate\Contracts\Support\Responsable interface.
Instead of forcing every controller action or custom object to implement a massive layout matrix, Laravel says: "If your class implements Responsable, it must contain exactly one method: toResponse($request)." When a controller returns that object, Laravel checks if it implements that specific interface and executes it automatically.
Summary for Laravel Developers
When creating custom classes, plugins, or traits in Laravel, ask yourself: "Is any class implementing this contract forced to write dummy return values or throw 'Not Supported' exceptions?"
If the answer is yes, break that contract up into smaller pieces immediately.
Top comments (0)