Dwi Wahyudi
Senior Software Engineer (Ruby, Golang, Java)
In this post we will write some code using Golang’s interface.
Overview
Interface is actually coming from static-typed OOP languages. In Golang (which is not a traditional OOP language), interface is just a set of methods signatures with no implementation code.
- Interface is usually used to denote a sharable and composable behavior.
- Interface is usually needed to write decoupled code, in Java, interface is the spotlight for some essential design patterns. With interface, we can split abstraction and implementation, the idea behind strategy pattern and repository pattern.
- With those patterns, code become more maintainable and adhere to open-closed principle. With interface contract, let say we want to get a user’s orders data, interface didn’t specify anything, it just specifies that such contract require a user’s id and return an array of orders objects. The implementation can be anything, whether it is from DBMS (MySQL, PostgreSQL), redis, API call, or even a testable mock.
- Interface defines/enforces contract (the contract is what we use to define a behavior), contract means: if a class/struct wants to implement a struct, it needs to implement all of the methods specified by such interface, so there will be no missing implementation. This is the default behavior of an interface.
- In Golang however, interface implementation is not explicitly defined, this is a common pattern in Go to use consumer-side interface, so that we only mock the needed methods only.
- Interface become even more helpful when it is related with collection (array, slice, list, etc), a group of data implementing the same interface can be iterated and processed together, although each implementation may differ.
- Interface can help developers write dependency injection.
- Interface can help developers write tests, by stubbing a data with help of an interface.
With interface and collection, be aware that the resulted code will look like traditional OOP.
For this article, we are going to make some common assumptions about our examples, there will be some classes here:
- Car is vehicle, a drivable, means we can find its fuel consumption. We can also define some other methods like turning left, turning right, gas, brake, etc, but for brevity let’s focus on fuel consumption only in this article.
- House has address.
- Store is like house, it has address, and store is made for selling something.
- Food truck is like a car, it is drivable (we can find its fuel consumption), and it sells something.
- Street vendor sells something, but it has no fuel consumption, and has no address.
Those 5 type of data share some behavior to each others, car is drivable, food truck is also drivable. Store sells something, food truck also sells something. We can identify 3 behaviors here:
- Drivable (something that we can drive and consume fuel for),
- Addressable (something we can give address to) and
- Shoppable (something we can shop from, because it sells something).
Let’s define those behaviors as interfaces, general rule for interface is that a class can have many behaviors (implement many interfaces) at once.
Interfaces
Here are our interfaces that define above behaviors:
// Drivable sets an interface for drivable vehicles.
type Drivable interface {
fuelConsumption() float64
}
// Addressable sets an interface for anything that can have address.
type Addressable interface {
description() string
}
// Shoppable sets an interface for anything we can shop from.
type Shoppable interface {
description() string
averageRating() float64
}
What will we do with this interface and its collection? In the future, probably we can do something with fuel consumption data, we can do something with those addressable entities, we can list the shoppable for potential customers, etc. With interface concept in mind, we can also create another interface that applies to all of those classes above, like Taxable, Acquirable, etc.
We need to make those 5 classes (as structs and methods) to implement those interfaces.
- Car implements
Drivable
. - House implements
Addressable
. - Store implements
Addressable
andShoppable
. - Food Truck implements
Drivable
andShoppable
. - Street Vendor implements
Shoppable
Structs Implementing the Interfaces
Here is how our Car
struct and methods look like:
type Car struct {
Mileage float64
FuelLiterPerMile float64
}
func (car Car) fuelConsumption() float64 {
return car.Mileage * car.FuelLiterPerMile
}
Notice that we set car
struct as the receiver for method fuelConsumption()
. This basically tells that we want to implement Drivable
interface by implementing its method.
And also notice how the function declaration is written,
// method definition
func (car Car) fuelConsumption() float64 {
This is a bit different than the procedural-style code:
// function definition
func fuelConsumption(car Car) float64 {
Let’s move to other structs and methods. Let’s move to House
:
type House struct {
OwnerName string
Address string
}
func (house House) description() string {
return "This house belongs to " + house.OwnerName + ", it is located at" + house.Address
}
Then Store
struct and its methods (we also add Product
struct here).
Here Store
struct implement both Addressable
and Shoppable
interfaces, but description()
method implementation is converging into one.
type Product struct {
Name string
Price float64
Currency string
Rating float64
}
type Store struct {
Name string
Address string
Products []Product
}
func (store Store) description() string {
return store.Name + " is located at " + store.Address + ", and it sells: " + productStrings(store.Products)
}
func productStrings(products []Product) string {
productString := ""
for _, product := range products {
priceString := strconv.FormatFloat(product.Price, 'f', 2, 64)
ratingString := strconv.FormatFloat(product.Rating, 'f', 1, 64)
productString += product.Name + " price: " + priceString + " with rating: " + ratingString + ", "
}
return productString
}
Then FoodTruck
which implements Drivable
and Shoppable
interfaces:
type FoodTruck struct {
ShopName string
Mileage float64
FuelLiterPerMile float64
Products []Product
}
func (ft FoodTruck) description() string {
mileageString := strconv.FormatFloat(ft.Mileage, 'f', 2, 64)
return "Food Truck: " + ft.ShopName + " has traveled " + mileageString + ", and it sells: " + productStrings(ft.Products)
}
func (ft FoodTruck) fuelConsumption() float64 {
return ft.Mileage * ft.FuelLiterPerMile
}
And finally StreetVendor
:
type StreetVendor struct {
OwnerName string
Products []Product
}
func (sv StreetVendor) description() string {
return "Street Vendor with owner: " + sv.OwnerName + " sells: " + productStrings(sv.Products)
}
Demonstrating the Code
We can then create a demonstration on using one of above class:
func main() {
foodTruck1 := FoodTruck{
"Great Awesome Cake",
450,
0.33,
[]Product{{"Strawberry Cake", 5, "USD", 9.2}, {"Orange Cake", 6, "USD", 8.9}},
}
fmt.Println(foodTruck1.fuelConsumption())
}
This will compile and run just fine, eventhough averageRating()
method isn’t implemented by those structs above.
Now let’s create other samples, and start to use use collection like slice:
func main() {
car1 := Car{23.65, 0.25}
truck1 := Car{349, 0.5}
house1 := House{"Jono", "Jalan Sukamaju no. 6 Jakarta Pusat, Jakarta"}
house2 := House{"John Smith", "2267 Buckhannan Avenue, North Syracuse, NY"}
store1 := Store{
"CV. Berkah Mandiri",
"Gang Mangga 2 no. 334, Jakarta Barat, Jakarta",
[]Product{{"Roti Buaya", 100_000, "IDR", 8.6}},
}
store2 := Store{
"George's Clothing",
"56F Tail Ends Road, Cape Girardeau, Missouri",
[]Product{{"Suit", 40, "USD", 8.6}},
}
foodTruck1 := FoodTruck{
"Great Awesome Cake",
450,
0.33,
[]Product{{"Strawberry Cake", 5, "USD", 9.2}, {"Orange Cake", 6, "USD", 8.9}},
}
streetVendor1 := StreetVendor{
"Mr. Roger",
[]Product{{"Super Duper Awesome Hot Dogs", 10, "USD", 9.4}},
}
// Drivable collection
drivables := []Drivable{car1, truck1, foodTruck1}
for _, drivable := range drivables {
fuelConsumption := drivable.fuelConsumption()
fmt.Println(fuelConsumption)
}
// Addressable collection
addressables := []Addressable{house1, house2, store1, store2}
for _, addressable := range addressables {
description := addressable.description()
fmt.Println(description)
}
// Shoppable collection
shoppables := []Shoppable{store1, store2, foodTruck1, streetVendor1}
for _, shoppable := range shoppables {
description := shoppable.description()
fmt.Println(description)
}
}
Take a closer look at above code samples, there are 3 collections there, we just want to print out some method calls to each struct.
drivables
is a collection (slice) of structs implementingDrivable
interface,addressables
is a collection (slice) of structs implementingAddressable
interface, andshoppables
is a collection (slice) of structs implementingShoppable
interface.
But this code won’t compile, because some structs haven’t implemented averageRating()
above. The slice literals won’t accept it. Just like a we clearly state above: The enforcement of contract will be realized once we use the interface type itself.
If we start to use the interface itself (Drivable), better check whether all of those structs implementing that interface already implemented all of the functions defined by the interface.
This case also happens in case we pass any drivable
to a function. This time store1
and foodTruck1
structs should’ve implemented all functions defined by Shoppable
interface, but they don’t.
func shoppableDescription(shoppable Shoppable) {
desc := shoppable.description()
fmt.Println(desc)
}
/* These won't compile. */
shoppableDescription(store1)
shoppableDescription(foodTruck1)
In order to successfully compile those code, we need to either implement averageRating()
method to all of those structs implementing Shoppable
interface, or for now just delete it. For this article, we will just comment averageRating()
method in Shoppable
interface.
We can then try to compile and run the code again:
In some cases, this preemptive interface is quite a problem for developers, because the contracts are defined ahead of the time. It depends on codebase situation and product requirements, but what if we don’t want to use average rating feature first, but we want to use some implementations of Shoppable
? This is where consumer-side interface helps.
Consumer-Side Interface
As we described above, in Golang, interface should be written in consumer-side, it means that there’s implementation codes without interface, and in consumer sides we create the interfaces for methods we need. This means that Drivable
, Shoppable
and Addressable
interfaces are placed on consumer-sides. If we want to reuse them in consumer packages, we can create a shared package called interfaces
, this way, generated mocks for such consumers unit tests will be small. This will also help us avoid big interface anti-pattern (interface pollution). In essence, the producers should not impose what kind of behaviors they impose (they should not provide interfaces), they only provide the implementations, let the consumer do the decoupling (by creating the interfaces needed).