Substitutability or the Liskov Substitution Principle (LSP) is a concept that I’ve tried to adhere to for some years when writing code. It’s beneficial for many reasons, but particularly when testing, as it can indirectly force you to write code that is more testable. Recently, I’ve started appreciating how it works in Go, and will step through how in this short article.
If you’re not familiar with the term, it was created by Barbara Liskov in 1987 and is effectively the following:
An object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program.
In modern PHP, the principle is pretty trivial to implement.
For example, when writing functions, instead of specifying a function parameter as a concrete type or a specific class, you specify an interface instead.
Then, so long as the parameter passed to the function implements said interface, then the function’s signature is satisfied.
Take the example below, where you can see what I mean.
<?php
interface SomeInterface {
public function someFunction(string $paramOne, string $paramTwo): string;
}
class SomeClass
{
public function someFunction(SomeInterface $param): string
{
return "some string";
}
}
The example above defines an interface named SomeInterface
which has a method named someFunction()
.
Then, it defines a class with a single function, named someFunction()
, which takes a SomeInterface
object.
By doing this so long as someFunction()
is passed an object that implements SomeInterface
, then the compiler won’t throw an error.
This approach makes the code easier to test, because you create a mock object with the applicable methods and pass it to the function during testing.
This avoids the need to properly instantiate a concrete object, one which may have a series of dependencies, such as a database connection which could be expensive and time-intensive to set up.
Gladly, substitutability is even easier (or more concise) in Go.
Take the following code example.
type Shortener interface {
Shorten() string
}
type App struct {
db *sql.DB
shortener Shortener
}
type SpyURLShortener struct {
}
func (s *SpyURLShortener) Shorten() string {
return "short"
}
app := App{db: db, shortener: shortener}
The code starts off by defining an interface named Shortener
which defines a single method named Shorten()
which returns a string.
Then, it defines a custom type, a struct named App
.
App
has two properties:
db
: a pointer to an sql.DB
object
shortener
: any object that implements the Shortener
interface.
After that, a custom type named SpyURLShortener
is defined.
This type implements a method named Shorten()
which returns a string.
Because it it defines the Shorten
method and the method has the same signature as that defined by the Shortener
interface, according to the Go compiler the type implements that the Shortener
interface.
Because of this, no explicit keyword is required to tell the compiler that a type implements a given interface, as the compiler can infer that information for itself.
So long as the type follows these two rules, then it implements a given interface.
That’s why Go interfaces make development and testing easier
To me, this is a more concise way of implementing the Liskov Substitution Principle than I’ve seen in other C-based languages (such as PHP and Java).
But, it does take some getting used to.
What’s more, at least initially, I feel that it increases cognitive load.
Given that, I still appreciate the explicitness of those languages, from time to time, when developing with Go.
By having an extends
or implements
keyword means that there’s no mistaking an object’s type or inheritance hierarchy.
However, Go’s implicit approach does afford it greater flexibility.
Said another way, it’s an excellent example of duck-typing, or:
If it walks like a duck and it quacks like a duck, then it must be a duck.
There’s more to Go interfaces…
If you’d like to dive deeper into Go Interfaces, then have a read on Go’s empty interface.
Join the discussion
comments powered by Disqus