When people learn about abstraction, they usually focus on the means used to achieve it: abstract classes, objects, functions, or interfaces.

Abstraction

They read that abstraction is used to hide background details or unnecessary implementation of the data so that users see only the required information.

It makes sense. When you drive a car you don’t care how it’s implemented, you only want to drive from Point A to Point B.

But abstraction is more than hiding implementation details: it’s about meaning. Dijkstra puts it better than me:

The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise.

A new semantic level in which one can be absolutely precise. I love this one.

We have done this ever since we emerged from our caves. That warm, fuzzy, butterfly feeling when you see a particular person? Let’s call it love and get on with it.

We take a blob of things, pack them under a new word, and we can start communicating better. Yes, the details are hidden, but now we can understand each other. A new semantic level in which one can be absolutely precise!

Enough talking. Show me some code! Link to heading

We have 3 teams: Cats, Dogs, and Beavers that compete with each other. competitions holds the matches and results shows who won(1 for winning, 0 for losing)

The winning team from each competition receives 3 points and the team that gets the most points is declared the winner.

Below we have the first implementation of the TournamentWinner that returns the winner of the tournament:

package main

import "fmt"

func main() {
	competitions := [][]string{
		{"Cats", "Dogs"},
		{"Dogs", "Beavers"},
		{"Beavers", "Cats"},
	}
	results := []int{0, 0, 1}
	// A team receives 3 points if it wins, and 0 points if it loses
	fmt.Println(TournamentWinner(competitions, results)) // Beavers
}

func TournamentWinner(competitions [][]string, results []int) string {
	var currentWinner string
	scores := make(map[string]int)

	for idx, competition := range competitions {
		homeTeam, awayTeam := competition[0], competition[1]

		if results[idx] == 1 {
			scores[homeTeam] += 3
			if scores[homeTeam] > scores[currentWinner] {
				currentWinner = homeTeam
			}
		} else {
			scores[awayTeam] += 3
			if scores[awayTeam] > scores[currentWinner] {
				currentWinner = awayTeam
			}
		}
	}

	return currentWinner
}

The code is hard to reason about. We have 4 clauses in that for statement, 4 things to keep in our heads. And what does that 1 mean again? Surely we can do better!

I wrote above that the winning team from each competition receives 3 points, but nowhere in my code do I see anything about the winning team, just a bunch of ifs. The winning team is the abstraction we need!

package main

const HomeTeamWon = 1

func TournamentWinner(competitions [][]string, results []int) string {
	var currentWinner string
	scores := make(map[string]int)

	for idx, competition := range competitions {
		homeTeam, awayTeam := competition[0], competition[1]

		winningTeam := awayTeam
		if results[idx] == HomeTeamWon {
			winningTeam = homeTeam
		}

		scores[winningTeam] += 3
		if scores[winningTeam] > scores[currentWinner] {
			currentWinner = winningTeam
		}
	}

	return currentWinner
}

Now we have only 2 if clauses, and we communicate much more clearly that we have a winning team and the winning team receives 3 points. 1 is now under a constant HomeTeamWon so we know what it means.

We went from 4 clauses to 2. Can we do better? Sure, we can abstract the way we get a winner under getWinner function and now our for statement has only 1 if:

package main

func TournamentWinner(competitions [][]string, results []int) string {
	var currentWinner string
	scores := make(map[string]int)

	for idx, competition := range competitions {
		winningTeam := getWinner(competition, results[idx])

		scores[winningTeam] += 3
		if scores[winningTeam] > scores[currentWinner] {
			currentWinner = winningTeam
		}
	}

	return currentWinner
}

const HomeTeamWon = 1

func getWinner(competition []string, result int) string {
	homeTeam, awayTeam := competition[0], competition[1]

	winningTeam := awayTeam
	if result == HomeTeamWon {
		winningTeam = homeTeam
	}
	return winningTeam
}

We could go even further and get rid of the last if by having a function that updates scores and returns the latest winner:

package main

func TournamentWinner(competitions [][]string, results []int) string {
	var currentWinner string
	scores := make(map[string]int)

	for idx, competition := range competitions {
		winningTeam := getWinner(competition, results[idx])
		currentWinner = updateWinner(winningTeam, scores, currentWinner)
	}

	return currentWinner
}

but I’m pretty happy with the previous state. There can be such a thing as too much abstraction. It’s the same with words: words are good, but when you communicate shorter is better. Getting to the right abstraction level is an art that comes with experience.

Conclusion Link to heading

I hope you liked this article and learned something new. Abstracting is not only about hiding implementation details behind an interface but also about meaning.

I have shown you that abstracting can be done using even one variable, making your code clearer and easier to reason about. And easy to reason about is key here. Because as Wittgenstein said:

The limits of my language mean the limits of my world.

Let that sink in!