Samy's blog

Today I Learned: SOLID - Interface Segregation Principle (ISP)

Today I Learned: SOLID - Interface Segregation Principle (ISP)

So Continuing on my list of learning about the SOLID principles:

Today I'm going to write about one that starts with an I, The forth principle states: Make fine grained interfaces that are client specific. or in other words:

A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

Example time: Suppose we have a struct that represents Documents, like this:

type Document struct {

}

and we want to do some operations with those documents, maybe print them or scan them? as good citizens, we create an interface to define such machine operations!

type Machine interface {
	Print(d Document)
	Fax(d Document)
	Scan(d Document)
}

and proceed to implement the interface:

// MultiFunctionPrinter is a printer with all the bells and whistles!
type MultiFunctionPrinter struct {
	// ...
}

func (m MultiFunctionPrinter) Print(d Document) {

}

func (m MultiFunctionPrinter) Fax(d Document) {

}

func (m MultiFunctionPrinter) Scan(d Document) {

}

what a machine this is!, it can Print, Fax, and Scan! we truly are living in the future ✨.

but what about a classic Printer, what will happen if the printer implements the Machine interface? let's take a look:

type ClassicPrinter struct {
	// ...
}
func (o ClassicPrinter ) Print(d Document) {
	// ok, all printers..by default, print
}

func (o ClassicPrinter ) Fax(d Document) {
	// ugg, this doesn't look nice
	panic("operation not supported")
}

func (o ClassicPrinter) Scan(d Document) {
	panic("operation not supported")
}

ugg, it has an operation that it cannot use, in other words, this classic printer is getting forced to implement methods it will not use!, and the principle states: A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

so how can we fix this?, well, we can split the Machine interface into more granularly defined ones:

//split into several interfaces
type Printer interface {
	Print(d Document)
}

type Scanner interface {
	Scan(d Document)
}

// printer only
type MyClassicPrinter struct {
	// ...
}

func (m MyClassicPrinter) Print(d Document) {
	// ...
}

as we can see, this new struct MyClassicPrinter is now truly a classic printer, the only operations it needs to define its..well.. Print, the only one it works with, and it only needs to implement the Printer interface, now our Classic Printer is not bogged down by all those methods it doesn't use!

Even better, if we want to define a device a does many things, like a Photocopier, it will just need to implement the interfaces with the operations it performs.

// implement multiple interfaces
type Photocopier struct {}

func (p Photocopier) Scan(d Document) {
	//
}

func (p Photocopier) Print(d Document) {
	//
}

A Photocopier can Scan and Print!

We can also take the following approach, by "combining interfaces":

type MultiFunctionDevice interface {
	Printer
	Scanner
}
type PhotocopierTypeB struct {}

func (p PhotocopierTypeB ) Scan(d Document) {
	//
}
func (p PhotocopierTypeB ) Print(d Document) {
	//
}

now anything that expects either a Printer an Scan or a MultiFunctionDevice (a combination of both)

can accept a PhotocopierTypeB!

func DoWork(mfd MultiFunctionDevice, d Document) {
	fmt.Println("scanning then printing!")
	mfd.Scan(d)
	mfd.Print(d)
}
func DoPrintStuff(mfd Printer, d Document) {
	fmt.Println("just printing!")

	mfd.Print(d)
}

func main() {
	photoCopier := PhotocopierTypeB{}
	doc := Document{}
	DoWork(photoCopier, doc)       //valid
	DoPrintStuff(photoCopier, doc) //valid
}

The full example can be found here

I hope anyone who is reading my short learning summaries finds them useful somehow...and as always:

A bit of tinkering a day keeps the boredom away. 🚀