Interface Segregation Principle

Take a tour through the history of dependency management in statically compiled languages. Learn why languages like C++ and Java need to explicitly build up small interfaces where Ruby is saved by duck typing, and then discover the lessons Ruby de...
This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/interface-segregation-principle

Interface types are an interesting feature in the Go programming language. They are meant to encourage creating abstractions by considering the behavior that is common between types, instead of the fields that are common between types.

They are types that define the methods their implementers need to define in order to implement the interface. Types that implement the interface do so implicitly, by defining those methods, rather than explicitly saying they 'implements InterfaceName` or something.

Go naming convention is to suffix ā€œ-erā€ (whenever possible. ā€œ-ableā€ is also common) to a type to indicate that it is an interface. So Shaper makes sense for shape interfaces. io.Writer is implemented by concrete types that implement Write.

Interfaces with only one or two methods are common in Go code, such as the famous http.Handler:

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter is also an interface. It provides access to the methods needed to return the response to the client. Those methods include the standard Write method, so an http.ResponseWriter can be used wherever an io.Writer can be used.

The entire database/sql package is an interface. You refer to it almost exclusively when using a concrete implementation such as lib/pq.

Hereā€™s a longer, common example:

package main

import "fmt"
import "math"

type Shaper interface {
  Area() float64
  Perimeter() float64
}

type Square struct {
  width, height float64
}

func (s Square) Area() float64 {
  return s.width * s.height
}

func (s Square) Perimeter() float64 {
  return 2*s.width + 2*s.height
}

type Circle struct {
  radius float64
}

func (c Circle) Area() float64 {
  return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
  return 2 * math.Pi * c.radius
}

func Measure(s Shaper) {
  fmt.Println(s)
  fmt.Println(s.Area())
  fmt.Println(s.Perimeter())
}

func main() {
  s := Square{width: 3, height: 4}
  c := Circle{radius: 5}

  Measure(s)
  Measure(c)
}

If we were to delete the Square.Perimeter() function, weā€™ll get a compilation error at Measure(s) like this:

cannot use s (type Square) as type Shaper in argument to Measure:Square does not implement Shaper (missing Perimeter method)

Using a few ~/.vimrc settings, we get this feedback immediately, when saving the file, which is awesome.

There is more great info about Goā€™s interfaces in Russ Coxā€™s ā€œInterfacesā€ article.

@jferris If I understand Interface Segregation Principle correctly, my Measure() function above would be in violation of ISP if I were to delete the fmt.Println(s.Area()) line, correct? At that point, the Shaper interface used in Measure() has a larger surface area of its interface than just the Perimeter() method that is uses.

That version would still compile in Go, but perhaps it would be more conventional to segregate the Shaper interface into Perimeterable and Areable interfaces. Would that also fix the ISP violation?

1 Like

Youā€™re right: having an interface (Shaper) where the client (Measure) only uses some of the interface is a violation of ISP, and splitting the interface would fix the violation.

Because Go has explicitly declared interfaces instead of duck typing, itā€™s might be more useful to think about ISP in Go than it is in Ruby. This is because the interfaces are enforced, so you wouldnā€™t be able to pass Square as a Shaper without implementing Area, even though it isnā€™t used. In Ruby, youā€™d still be able to pass a Square to Measure.

Iā€™d argue that the problem still exists in Ruby, though, and that Go is just structured so that you have to fix the problem. Hereā€™s a Ruby example:

class Square < Struct.new(:length)
  def area
    length * length
  end

  def half
    Square.new(length / 2)
  end
end

class Rectangle < Struct.new(:height, :width)
  def area
    height * width
  end
end

def measure(shaper)
  puts shaper.area
end

def measure_half(shaper)
  puts shaper.half.area
end

measure Square.new(5)
measure Rectangle.new(5, 10)
measure_half Square.new(10)

Even though this will work, itā€™s confusing that sometimes a shaper is something with just area, and sometimes it needs both area and half. Ruby will let you run this confusing code, but a similar example in Go will refuse to compile until you clarify the interfaces.

So I just finished this weeks iteration. I am a fan of SOLID and I am guilty of ignoring looking into the I much. Now I feel like a fool.

This explains one of my most beloved principles, something I catch a lot of flack from other devs. Being very minimal in the dependencies I choose to expose my objects to. I donā€™t like dependencies because of the coupling.

I donā€™t like coupling because it makes code hard to change, and test. Without even knowing it, I am also following the I. How cool. I havenā€™t thought in terms of Interfaces explicitly since I left C# many years ago.

I just became my second favorite piece of solid code :slight_smile:

Thanks Joe!

Even though this topic and discussion is quiet old, I just wanted to say, that pointing out that exposing large
methods surfaces is also a kind of violation to the ISP, is really important.
Since as soon as someone hooks into one of the unnecessary methods, its the same as having an interface class with a unneeded method.
Joe and Ben pointed out the ruby always uses the smallest interface possible, which is right, but on the other hand,
the developer exposes the maximum possible interface to the client of the class. Thus creating lots of possibilities to
violate ISP and to couple code and increase the number of reasons the code would have to change.