DEV Community

Aljosha Papsch
Aljosha Papsch

Posted on

Showcasing higher order functions with HTTP router

The other day I was talking to a friend who was deep in the woods reworking a piece of software. There was a global map involved, where the keys are HTTP routes and values are functions. Plugins would add their routes to the map by assigning a function reference. Then a core setup function would register all routes from the map with the actual HTTP router. The purpose for the map wasn't quite clear to me and the friend told me some plugins conflict with each other (e.g. login with Google vs login with Microsoft). A core setup function would decide which of the conflicting plugins actually to use.

Maybe there's an alternative to the global map. Just let the plugins add their routes directly to the router! But what about the setup function discriminating on arbitrary conditions? Let the plugins decide for themselves! Enter higher order functions, which are defined as functions taking function type arguments or returning function types.

Registering routes without direct router dependency

To add routes, assume we have a nifty library providing a router struct.

func (a *NiftyRouter) Register(method, path string, handler func())
Enter fullscreen mode Exit fullscreen mode

On the other hand there's a bunch of plugin functions eager to add routes. To avoid strongly coupling plugins and the concrete router struct, let's define a function type that expresses the intent of registering a route:

type RegisterRouteFunc func(method, path string, handler func())
Enter fullscreen mode Exit fullscreen mode

An implementation using the concrete router struct is a higher order function returning a function matching RegisterRouteFunc signature:

func RegisterRouteNifty(router *NiftyRouter) RegisterRouteFunc {
    return func(method, path string, handler func()) {
        router.Register(method, path, handler)
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice how the router argument is used in the returned function, creating a closure. The performance penalty of closure creation is negligible because RegisterRouteNifty is called only once early in program runtime (more on that later).

Defining the plugin interface

In the same vein, create a function type for the intent of adding a plugin. The function receives an argument of RegisterRouteFunc, conveying the fact that plugins can register HTTP routes.

type AddPluginFunc func(register RegisterRouteFunc)
Enter fullscreen mode Exit fullscreen mode

Each plugin provides an implementation of AddPluginFunc:

func AddProfilePagePlugin() AddPluginFunc {
    return func(register RegisterRouteFunc) {
        register("GET", "/profile", func() {
            // ...
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

If there's some need for arbitrary conditions, add the required inputs as arguments to the named function. This creates another closure, but again the impact is negligible.

func AddGoogleLoginPlugin(config *Configuration) AddPluginFunc {
    return func(register RegisterRouteFunc) {
        if (config.Foo) {
            register(/* ... */)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

There might also be a AddPluginFunc function that composes several AddPluginFuncs into one function:

func AddPlugins(plugins ...AddPluginFunc) AddPluginFunc {
    return func(register RegisterRouterFunc) {
        for _, f := range plugins {
            f(register)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Putting it together

With the building blocks in place, the main function can be assembled:

func main() {
    // Some requirements
    var config *Configuration
    var router *NiftyRouter

    register := RegisterRouteNifty(router)

    AddPlugins(
        AddGoogleLoginPlugin(config),
        AddMicrosoftLoginPlugin(config),
        AddProfilePagePlugin(),
        // ...
    )(register)

    // Start the router...
}
Enter fullscreen mode Exit fullscreen mode

Of course, higher order functions can be used in many different circumstances. Some care has to be taken when creating closures to avoid performance penalties.

(The code examples were all written for this post.)

Top comments (0)