Interface Segregation Principle

Avoid Non-Essential Knowledge

Posted by Jake Corn on May 2, 2020

(This is just my take on it. It's a great solid principle that I practice in my work. As an added bonus, it is very easy to start practicing.)

TL;DR; Limit your code to the minimal knowledge it needs to get its work done. Unnecessary knowledge can pervade systems and make them brittle and difficult to change.

Interface Segregation Principle (ISP) is the I in the SOLID acronym used to describe Clean Code. I've learned about SOLID from Robert Martin who authored the book Clean Code.

The following examples are TypeScript.

Imagine we have a function that when given a Car entity will produce a description string about it. (It's a trivial example)

class Car {
  //...
  year: number
  make: string
  model: string
}

const getDescription = (car: Car): string =>
  car.year + ' ' + car.make + ' ' + car.model

then in a calling method of some class...

buildInventoryReport(): InventoryReport {
  //...
  for (const item of this.items) {
    // item is a "car" in this scenario
    report[item.id] = getDescription(item)
  }
  return report
}

What's the problem here? Well, Transitively, we've forced the InventoryReport building method to know about Car when really it only depends on an object with year, make, and model.

A car might have wheels, radio, etc... that the Inventory Report doesn't care about.

This might not seem bad... but does the caller of Inventory Reporting need to implement Car...?

maybe (probably)

What if someone wanted to use InventoryReport to build a report of all bicycles?

That sounds like a valid use case for InventoryReport. After all, a bicycle has a year, make and model.

but... getDescription is now forcing its caller to implement Car. A Car is not a bicycle.

You now have to work back to break the unneeded transitive dependency. Maybe with an interface named Product (a better name might be possible, forgive me wanting to move on and get the example done.)

interface Product {
  year: number
  make: string
  model: string
}

const getDescription = (p: Product): string =>
  p.year + ' ' + p.make + ' ' + p.model

So reuse is increased by being able to use this for different things that have year, make, and model.

Also, as a developer, your function now knows about less, which makes it more durable and easier to debug. This also makes it more flexible as the idea of product description is not logically tied to the idea of a car.

So... what about dynamic languages... does this matter without static type checking?

in js (another trivial example)

// I'm somewhat regretting using concatenation as the example driver, since it would
// all would be better with a variadic concat function
const getFullName = ({ entity }) =>
  return entity.name.first + ' ' + entity.name.last

in a caller

const fullName = getFullName({
  entity: {
    name: {
      first: 'Billy',
      last: 'Bob'
    }
  }
})

The above function has a lot of information it definitely did not need... so now callers have to glue together this object with an entity property just to get a full name.

so, knowledge about implementation leaks into callers in dynamic languages too.

Limit your code to the minimal knowledge it needs to get its work done. Doing more makes it brittle. Don't fear repetition if two separate interfaces end up looking the same. (They are not be the same if they don't change at the same time for the same reason.) The ability to write well encapsulated, testable code is much more important/valuable than the alluring ease of reusing a bloated interface.

-- jake