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:
- S - Single-responsiblity Principle
- O - Open-Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle (you are here)
- D- Dependency Inversion Principle
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. 🚀