POSTS
Why and how you should code to interfaces - and high-order functions
- 9 minutes read - 1814 wordsOne famous debate that emerges in every design discussion is about interfaces. As always, each programmer has a different conception about it and it’s something almost inevitable to spend hours arguing about. Even when the discussion isn’t about whether or not we should use it, it’s about how to use it. As a programmer, I have my own opinion as well. I expect to give you an overview about the importance of interfaces when you should or not use it, and what you can apply it to.
I won’t define here what an interface is, because I believe that you can have way better definitions out there in the web. However, it’s important to know that we are going to talk about programming language interfaces and if you have no idea what it is, you should search for it first .
Why you should use interfaces
If I was asked to summarize the importance of interfaces in one sentence, I would say that interfaces allow you to inject behavior in your methods and classes. Of course, there’s so much more to talk about and I’m planning to do it in the following sections, but that is the main reason that you should use interfaces while programming.
Onto the reasons, then:
1) Interfaces allow you to inject behavior
For me, the best example of behavior injection is the “Comparable” interfaces in Java and C# or the “sort.Interface” in Go. Sorting a collection is a well-known problem in computer science and we have a lot of different algorithms to do this. And all of those algorithms rely on the same basic function: how to compare different elements in the collection. When we create or code a sorting algorithm, we don’t care how to compare different elements, we don’t do this because we don’t know which elements we are going to compare (their type) or how they can relate to each other. So, while coding those, we expect to receive a comparator injected in our method or our class and then just use this provided behavior.
// Insertion sort
func insertionSort(data Interface, a, b int) { // Our original code
for i := a + 1; i < b; i++ { // Our original code
for j := i; j > a && data.Less(j, j-1); j-- { // Our code + provided behavior
data.Swap(j, j-1) // Our original code
}
}
}
Source: Go source code
It’s impossible to make a code as simple as that without relying on injected behavior. And, considering that you are creating your source code before your API user, it’s obvious that you can’t have a regular class passed to your method, you need an interface (or an abstract class) so your user could implement it and inject that specific Less/Swap behavior.
Another good example of injected behavior is strategy pattern. This design pattern defines that you can change the implementation (strategy) that will be executed at runtime based on program state (variables and etc). So, to create it you’ll need an interface (or an abstract class) with two or more different implementations, inject then in your main class and then switch between them based on your state.
type Operator interface {
Apply(int, int) int
}
type Operation struct {
Operator Operator
}
func (o *Operation) Operate(leftValue, rightValue int) int {
return o.Operator.Apply(leftValue, rightValue)
}
Source: https://github.com/tmrts/go-patterns/blob/master/behavioral/strategy.md
2) Interfaces force you to [re]think about your code’s behavior
As I said, the main purpose of interfaces is to allow you to code based on behaviors. So, if you start using them you will be forced to rethink your code before writing it. This is really important because when you write your code based on behaviors you make it more declarative than imperative, so it’s easier to read, maintain and improve. I’m not saying that you need to start coding right away from interfaces. I think you can have interfaces that emerge from your code after a while. But, to do so it’s important to reserve some time to reflect and rethink your code. It’s really important to stop for a while and make some improvements like behavioral extraction into interfaces. Your code should be the easiest to read as possible, so it needs to be neat and clean.
3) Interfaces make you code with a more reasonable and understandable API
If we take a look at all open source libraries, we will see that they usually define their API using interfaces. Take the Apache Beam repository as an example. If we perform a simple search, we can see it has 849 interface keywords and 3,806 class keywords. It means that approximately 18% of the repo is composed of interfaces, it’s almost one interface for every 4 classes. Considering that coding to interfaces has been an industrial practice, new programmers that join a team expect to see them in the code base. So it’s a common task to start to read a code by its APIs, thus, by reading its interfaces. So, if you avoid using interfaces because you think you don’t need them or because you think it’s a waste of time, you are probably saving your own time in the short term, but making all other programmers’ life quite hard. Also, I really don’t believe that you’ll save time skipping interfaces when they are needed, because, as I said, they force you to think before action and they make your code more descriptive. However, if you still think it’s useless, at least think about the suffering that you may cause to your fellow coworker who will need to open the source file and read your entire class to understand how to use it correctly.
4) Interfaces make your code more testable
This topic is more a consequence of the three other topics. Of course, you can mock and test your code using only structs/classes, and don’t get me wrong here, I’m not saying that you should create interfaces just for the sake of testability. However, abstractions will indeed make your code more testable.
How to code to interfaces
There’s no golden rule here. Sometimes, it’s easier to start from an interface signature, but there are times that interfaces will emerge from an already existing code. The main point is: you don’t necessarily need to start from an interface every time you code, but if you don’t, you have to refactor your code as soon as possible to extract inner behavior and make the outer class with a single responsibility (as SOLID states). Personally, I like to stop and think before coding. When I know the entities and models that I’m working on, I tend to start with some interfaces to extract common or mutable behavior. However, we sometimes have no idea what to code, we don’t know our models and/or entities, so it’s easier to code with few interfaces and refactor later with more business knowledge. One last tip to code using interfaces is to receive an interface and return a concrete type. If you do so, you’ll make your code more general and easier to use, because your client won’t need to create adapters from their classes to yours. They will only need to implement an interface and it’s so much easier to do it than to create a whole new adapter class. The bottom line is: it doesn’t matter how you code, as long as you refactor it.
Be aware of High-Order Functions
So far, I have been saying that you should use interfaces to inject behavior in your classes, however we can do this using high-order functions as well. Pure functional languages as Haskell and some more modern languages like Kotlin, Scala and Go have high order functions, i.e., functions that take one or more functions as parameters and/or return a function. Even languages without high-order functions can simulate it using lambdas with functional interfaces (interfaces with only one method). So, almost every time we have a functional interface, we can achieve the same behavioral-injection results using functions and closures. Take a look at the example below, it is very similar to the sorting example, but instead of receiving an interface as a parameter, the method is receiving a function “f”, so it can delegate some behavior to this function. The method doesn’t know (and care) how to select an element, it just iterates over them and delegates this behavior to the function. It’s easier to make a SOLID code using these features.
func filter(s []student, f func(student) bool) []student {
var r []student
for _, v := range s {
if f(v) == true { // Delegating behavior to “f”
r = append(r, v)
}
}
return r
}
Source: https://golangbot.com/first-class-functions/
Of course, we can change the “f” function to a “Predicate” interface with only one method, but this will probably result in more code. Also, in the end, you will be passing data with behavior instead of behavior alone.
So, every time that you create a interface with just one method, double check whether you can change it to a function.
If you want to learn more about it, check this post by Cheney.
Avoid interface overuse and interface pollution
There are two common errors that you should avoid when coding an interface: overuse and pollution.
Interface overuse
As its name suggests, interface overuse occurs when we create too many interfaces, for example, when we create a new interface for every new class, as we see a lot in Spring Services. We have to keep in mind that interfaces should be used to inject behavior and make our code clean and maintainable. If they are making it harder to read and/or harder to maintain, then we should rethink and refactor our code.
Interface pollution
The bigger the interface, the weaker the abstraction. — Rob Pike, Go Proverbs
Interface pollution is the case when you have huge interfaces. Like classes, interfaces should deal with only one responsibility, so they have to be small and self contained. Besides, as the SOLID principle states, “Clients should not be forced to depend upon interfaces that they do not use”, so, even though your concrete type can be large, your client API should be small and easy to understand. If you have an interface with more than three methods, you should check whether it will need a refactor into smaller pieces.
If you want to learn more about pollution, check this link out.
Conclusion
As almost everything in software engineering, interfaces are a complex and controversial topic. There’s no golden rule dictating when and how to use them; however, if you are not using them at all, you are probably doing it wrong. So, let’s stop coding now and start refactoring all those bloated codes that you complain about all the time. Let’s refactor them looking for behaviors to be externalized and new ways to make them more descriptive, clean and maintainable.