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())
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())
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)
}
}
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)
Each plugin provides an implementation of AddPluginFunc
:
func AddProfilePagePlugin() AddPluginFunc {
return func(register RegisterRouteFunc) {
register("GET", "/profile", func() {
// ...
})
}
}
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(/* ... */)
}
}
}
There might also be a AddPluginFunc
function that composes several AddPluginFunc
s into one function:
func AddPlugins(plugins ...AddPluginFunc) AddPluginFunc {
return func(register RegisterRouterFunc) {
for _, f := range plugins {
f(register)
}
}
}
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...
}
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)