Skip to main content

Command Palette

Search for a command to run...

Interface Segregation Principle (explained in Go)

Updated
4 min read
Interface Segregation Principle (explained in Go)

The Interface Segregation Principle is the I in SOLID design principles. It states that types should not implement methods unrelated to their operations. In essence, interfaces should be designed so that the implementing types are not forced to implement a method they do not need. Here is an example:

type Device interface {
    TurnOn() error
    TurnOff() error
    GetStatus() string
    SetTemperature(temperature float64) error
}

The Problem

With the current way this interface is designed, all implementations have to implement all methods in the interface, even if they do not need it (e.g. TV).

This is bad because an implementation of this interface has to add a needless function that returns a useless error (TVs don’t have temperature settings, so it shouldn’t have that method in the first place!).

type TV struct {
    Status string
}

func NewTV() *TV {
    return &TV{}
}

func (tv *TV) TurnOn() error {
    tv.Status = "On"
    return nil
}

func (tv *TV) TurnOff() error {
    tv.Status = "Off"
    return nil
}

func (tv *TV) GetStatus() string {
    return tv.Status
}

func (tv *TV) SetTemperature(temperature float64) error {
    return errors.New("SetTemperature not applicable for lights") // or panic("not implemented")
}

The Fix

To fix this problem we need to separate the features in a way that we can couple implementations such that needless functions are not implemented awkwardly.

This “segregation” makes more sense, assuming all devices can be turned on and off (and we need to know if the device is on or off). Also, not all devices manage temperature, meaning we can have implementations of Device that also implement ManagesTemp or simply implementations of only ManagesTemp without power (like a bucket of ice poured over your head or a root cellar 😂).

type Device interface {
    TurnOn() error
    TurnOff() error
    GetStatus() string
}

type ManagesTemp interface {
    SetTemperature(temperature float64) error
}
// TV - implements Device
type TV struct {
    Status string
}

func NewTV() *TV {
    return &TV{}
}

func (tv *TV) TurnOn() error {
    tv.Status = "On"
    return nil
}

func (tv *TV) TurnOff() error {
    tv.Status = "Off"
    return nil
}

func (tv *TV) GetStatus() string {
    return tv.Status
}
// AC - implements both Device and ManagesTemp
type AC struct {
    Status      string
    Temperature float64
}

func NewAC() *AC {
    return &AC{}
}

func (a *AC) TurnOn() error {
    a.Status = "On"
    return nil
}

func (a *AC) TurnOff() error {
    a.Status = "Off"
    return nil
}

func (a *AC) GetStatus() string {
    return a.Status
}

func (a *AC) SetTemperature(temperature float64) error {
    a.Temperature = temperature
    return nil
}

Complete Usage

With our current implementation, we can create a list of devices and only use the temperature feature if it is implemented. Say we have many devices, we can write a function to turn all our devices on and set the temperature of the house to 22°C all in one loop:

type Device interface {
    TurnOn() error
    TurnOff() error
    GetStatus() string
}

type ManagesTemp interface {
    SetTemperature(temperature float64) error
}

type Devices []Device

func PowerOn(devices Devices) {
    for _, device := range devices {
        if device.GetStatus() != "On" {
            device.TurnOn()
        }
        // For each Device, we check if it has temp management feature
        if ac, ok := device.(ManagesTemp); ok {
            ac.SetTemperature(22.0)
        }
    }
}

This complete our clean implementation and usage of Interface Segregation to ensure that we don’t have implementation of functions that aren’t required, this gives flexibility to use correct interface as required. We can create a list of Devices thus:

var devices = Device{}
devices = append(devices, NewAC())
devices = append(devices, NewTV())
...

Conclusion

In essence, ISP offers these key benefits: Reduced Coupling: By breaking down interfaces into smaller, focused ones, dependencies between different parts of your code are minimized, meaning changes to one interface are less likely to affect other system parts. Improved Reusability: These interfaces are more likely to be reusable in various system parts or other projects. Easier Maintenance: Changes to a specific interface have a smaller impact, making the code easier to maintain and evolve over time. Enhanced Testability: Smaller interfaces are easier to test in isolation, resulting in more efficient and reliable unit tests. In essence, ISP promotes a more modular and flexible design, making your code easier to understand, maintain, and extend.