Anybody who has worked with me knows that I’m a fan of the original Design Patterns book, I remember buying it more than 20 years ago in the old Borders in the centre of Glasgow, and reading it and thinking it was a bit dry to read, but really interesting, I have a memory of implementing the State pattern in Python with the old Python Tk bindings in a job I left in June 2000.

I do accept that folks can tend to overuse the patterns, and that as time as passed, they’ve not all stood the test of time as well.

But, they do provide a useful framework for discussing things, the words “factory”, “builder”, “observer”, “visitor” and “singleton” are all easily recognisable in software engineering, thanks in large part to the success of the “Gang of Four” book.

One of the most recognisable patterns is the “Adapter” pattern, and it’s one I use a lot in the types of projects I’ve been involved in over the past few years, where systems need to talk to different (but similar) things, it’s also core to the Hexagonal Architecture, in its “Ports and Adapters” alias.

From pg.139 of the original Design Patterns book:

Intent

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

Also Known As Wrapper

The original chapter talks about “Class adapters” and “Object adapters”, and it’s “Class adapters” I’ll talk about here.

Adapter UML Diagram

Basically, clients talk to an object that implements an interface (in this example it’s Operator), and they delegate to another class (the Adaptee) and call a method on that to do the work.

From the client’s point of view, it’s isolated from the SpecificOperation, it can talk to any implementation of the Operator interface, and it should adhere to the interface contract.

Non-adapter version

Adapters almost always come from refactoring code, and changing requirements, it’s pretty rare in my experience to start with one.

So, I’ll implement some simple functionality, and look at how the pattern helps simplify the behaviour.

For this example, I’ll illustrate by reading a file from a repo in Github, println will stand-in for actually processing the file that’s read.

Using GitHub API For Java in Kotlin, it looks like this:

package com.bigkevmcd.client

import org.kohsuke.github.GitHub

fun getFile(repo: String, ref: String, filename: String): String {
    return GitHub
        .connectAnonymously()
        .getRepository(repo)
        .getFileContent(filename, ref)
        .read()
        .readAllBytes()
        .toString(Charsets.UTF_8)
}

fun main(args: Array<String>) {
    println(getFile("bigkevmcd/github-tool", "master", "README.md"))
}

Without the nice spacing, this is only six lines of code, pretty short and simple.

In the main, the println doesn’t know about how that code is fetching the file, it just calls getFile with the correct parameters.

A nice, simple way of reading files from GitHub…and then someone comes along and asks for GitLab support (yeah, customers), you argue that “everybody uses GitHub anyway”, but to no avail, you’ve got to support GitLab (just be glad nobody’s asked for Bitbucket yet).

A simple attempt might look like this, using GitLab4j.

import org.gitlab4j.api.GitLabApi

fun main(args: Array<String>) {
    val repo = "bigkevmcd/github-tool"
    val ref = "master"
    val filename = "README.md"

    val readme = when(args[0]) {
        "github" -> GitHub
                        .connectAnonymously()
                        .getRepository(repo)
                        .getFileContent(filename, ref)
                        .read()
                        .readAllBytes()
                        .toString(Charsets.UTF_8)
        "gitlab" -> GitLabApi("https://gitlab.com/", System.getenv("GITLAB_TOKEN"))
                        .repositoryFileApi
                        .getFile(repo, filename, ref)
                        .decodedContentAsString
        else -> "unknown git host"
    }
    println(readme)
}

This works, but it can definitely be improved:

enum class GitHost {
    GitHub, GitLab
}

fun getFile(type: GitHost, repo: String, ref: String, filename: String): String {
    return when(type) {
        GitHost.GitHub -> GitHub
                        .connectAnonymously()
                        .getRepository(repo)
                        .getFileContent(filename, ref)
                        .read()
                        .readAllBytes()
                        .toString(Charsets.UTF_8)
        GitHost.GitLab -> GitLabApi("https://gitlab.com/", System.getenv("GITLAB_TOKEN"))
                        .repositoryFileApi
                        .getFile(repo, filename, ref)
                        .decodedContentAsString
    }
}

fun main(args: Array<String>) {
    // This should use args to pick a GitHost enum
    println(getFile(GitHost.GitHub, "bigkevmcd/github-tool", "master", "README.md"))
}

And this clearly works well enough, but the println has gone from not knowing how to get the file, to passing it in.

It’s a bit tricky to work out how I’d customise the GitHub or GitLab endpoints tho’, so I might refactor it to something like:

class GitClient(val github: GitHub, val gitlab: GitLabApi) {
    fun getFile(type: GitHost, repo: String, ref: String, filename: String): String {
        return when(type) {
            GitHost.GitHub -> github
                            .getRepository(repo)
                            .getFileContent(filename, ref)
                            .read()
                            .readAllBytes()
                            .toString(Charsets.UTF_8)
            GitHost.GitLab -> gitlab
                            .repositoryFileApi
                            .getFile(repo, filename, ref)
                            .decodedContentAsString
        }
    }
}

fun main(args: Array<String>) {
    val client = GitClient(GitHub.connectAnonymously(), GitLabApi("https://gitlab.com/", System.getenv("GITLAB_TOKEN"))

    println(client.getFile(GitHost.GitHub, "bigkevmcd/github-tool", "master", "README.md"))
}

This would at least allow configuration of the authentication and endpoint, but the println still needs to know what type to use for the client.

At this point, an adapter becomes really useful:

Adapter version

interface GitHost {
    fun getFile(repo: String, ref: String, filename: String): String
}

This is the basic interface, that I want to create adapters for (in the “Ports and Adapters” architecture, types have their dependencies expressed as interfaces, and these are the ports).

Essentially we’re saying that a GitHost provides a method getFile and it’s entirely up to the GitHost implementation to fulfil the contract.

Creating the adapter to implement the interface reuses the existing code:

class GitHubHost(val github: GitHub) : GitHost {
    override fun getFile(repo: String, ref: String, filename: String): String {
        return github
            .getRepository(repo)
            .getFileContent(filename, ref)
            .read()
            .readAllBytes()
            .toString(Charsets.UTF_8)
    }
}

fun main(args: Array<String>) {
    val client = GitHubHost(GitHub.connectAnonymously())

    println(client.getFile("bigkevmcd/github-tool", "master", "README.md"))
}

The GitHubHost class adapts the GitHub class, to the GitHost interface.

Note that in the println it’s just calling getFile, and it doesn’t really have to care about what type of host it’s talking to.

The GitLab case is really simple to implement too, reusing the code from earlier:

class GitLabHost(val gitlab: GitLabApi) : GitHost {
    override fun getFile(repo: String, ref: String, filename: String): String {
        return gitlab
            .repositoryFileApi
            .getFile(repo, filename, ref)
            .decodedContentAsString
    }
}

Now, the GitLabHost class adapts the GitLabApi class, to the GitHost interface.

fun main(args: Array<String>) {
    val client = GitLabHost(GitLabApi("https://gitlab.com/", System.getenv("GITLAB_TOKEN"))

    println(client.getFile("bigkevmcd/github-tool", "master", "README.md"))
}

The println line hasn’t changed at all, because the the client is adapting the underlying GitLab API.

One thing that I have seen before, if you know you’ll want multiple adapters, it’s definitely easier to develop these in parallel, to avoid the conventions of one of the Adaptees leaking into the “port” interface, I’ve seen cases where one implementation is the first one to be completed, and when it comes time to build the second, it’s much harder to fit the API.