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?
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
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.