If a remote control's power button turns the TV off instead of on, it doesn't matter that it's shaped like every other remote — it breaks the contract callers rely on. A subtype has to honor the behavior its parent type promises, not just match its shape.
Working across ERPs with different backing stores — Openbravo on Oracle, an internal PHP/PostgreSQL toolset at Ronda, InformaWeb on MySQL at RSN — taught me to be wary of an interface that promises more than every implementation can actually deliver. Liskov Substitution is the discipline behind that: if a repository interface promises 'save', every implementation must genuinely support saving, or the interface needs to be split narrower rather than letting one implementation quietly break the contract.
Trade-off: splitting one interface into a reader and a writer means every caller now has to be explicit about which one it actually needs — code that used to just ask for 'the repository' now has to decide upfront whether it only reads or also writes.
Shape
Before
interface InventoryRepository
{
public function save(Item $item): void;
}
class CachedInventoryRepository implements InventoryRepository
{
public function save(Item $item): void
{
throw new RuntimeException('Cache repository is read-only');
}
}After
interface InventoryReader
{
public function find(int $id): Item;
}
interface InventoryWriter extends InventoryReader
{
public function save(Item $item): void;
}
class CachedInventoryRepository implements InventoryReader
{
public function find(int $id): Item { return $this->cache->get($id); }
}