SOLID: The Interface Segration Principle (ISP)

Clients should not be forced to depend on methods they do not use.

August 12, 2021

Introduction

Robert C. Martin coined the mnomonic acronym SOLID to represent 5 principles of software design. This is the fourth in my own five-part series exploring my understanding of and experience applying these principles:

  1. [SRP] The Single-Responsibility Principle:
    • "Software entities should have only one reason to change."
  2. [OCP] The Open–Closed Principle:
    • "Software entities should be 'open for extension but closed for modification'."
  3. [LSP] The Liskov Substitution Principle:
    • "Subtypes must be substitutable for their base types."
  4. [ISP] The Interface Segregation Principle: (you are here)
    • "Clients should not be forced to depend on methods they do not use."
  5. [DIP] The Dependency Inversion Principle:
    • "High-level policy should depend upon abstractions, not concretions."

ISP

Consider a program that transforms markdown text files into html files. (Hey, like the very one used to render the HTML you are now reading!) That program would need to interact with actual files on a file system at some point, both reading and writing:

type FileSystem interface{
	ReadFile(path string) ([]byte, error)
	WriteFile(path string, content []byte, perm os.FileMode) error
	MkdirAll(path string, perm os.FileMode) error
	Walk(root string, walk filepath.WalkFunc) error
}

But, if we are splitting components of this system into small parts (according to SRP) it is likely that very few of those parts will need the entire set of behavior defined by the file system interface. Why not split them up?

type (
	ReadFile interface {
		ReadFile(path string) ([]byte, error)
	}
	WriteFile interface {
		WriteFile(path string, content []byte, perm os.FileMode) error
	}
	MkdirAll interface {
		MkdirAll(path string, perm os.FileMode) error
	}
	Walk interface {
		Walk(root string, walk filepath.WalkFunc) error
	}
)

But there may be components that do depend on some or all of the above behavior, so compose a few more interfaces from the above building blocks. How about an interface for the component that reads html templates from the file system?

type TemplateLoaderFileSystem interface {
	ReadFile
	Walk
}

How about an interface for the component that writes rendered html files to disk:

type RenderingFileSystem interface {
	MkdirAll
	WriteFile
}

The slimmer the interface, the easier it is to mock.

BTW, the production implementation of all the interfaces above is quite simple:

type Disk struct{}

func (Disk) ReadFile(path string) ([]byte, error) {
	return os.ReadFile(path)
}
func (Disk) WriteFile(path string, content []byte, perm os.FileMode) error {
	return os.WriteFile(path, content, perm)
}
func (Disk) MkdirAll(path string, perm os.FileMode) error {
	return os.MkdirAll(path, perm)
}
func (Disk) Walk(root string, walk filepath.WalkFunc) error {
	return filepath.Walk(root, walk)
}

Define interfaces in the slimmest terms possible. Your users will thank you later.


Next Section: The Dependency Inversion Principle

-Michael Whatcott