Extensibility and Declarative Programming

When you have a hammer, everything looks like a nail.
When you have a hammer, everything looks like a nail.

When I first got into CoffeeScript, one of the main factors that got me over my resistance to learn a foreign (Ruby-inspired) syntax was that it provided a way to use prototypal inheritance without manually typing `MyObject.prototype.method` or endlessly worrying about what `this` currently means. In a surprising twist, the feature that got me into CoffeeScript is one I no longer use. Instead of writing OOP I have started writing in a more functional style, and have found CoffeeScript to be equally powerful (fat and skinny arrow function shorthand, implicit returns, fewer curly braces and parentheses to close, etc). ES6 has added `class` syntax sugar, but for me, it is a feature I will generally avoid in either language.

Imperative Extensibility

In developing a foundation for an extensible application, I started with a base plugin with helper functions. Plugins would import the base plugin class and call those helper functions to change configuration. From a framework perspective, the goal should be to minimize boilerplate—code that is identical in every place a feature is used. Boilerplate wastes the time of plugin authors, and increases the complexity of plugins with code that is not unique to the problem being solved by the plugin. Helper functions are a great way to reduce boilerplate in an imperative system.

Boilerplate and Housekeeping

Boilerplate is not the only concern. Plugin authors should also be spared of as much housekeeping as possible. Housekeeping is the management of state, through operations to add, update, or remove objects (configuration, functions, etc). In an imperative system, a plugin author is responsible for performing the operations required to add functionality to the system, but must also be concerned with updating or removing functionality. These operations are made even more complex with dependencies, as the order of the operations matters. Suddenly, leaving housekeeping up to the plugin author can turn into a massively complex and error-prone system.

Functional Design in an Imperative World

After studying functional programming languages and concepts, it became clear to me that a more graceful way of handling extensibility and mitigating complexity would be to forgo the object prototype-based imperative approach in favor of a declarative approach.

For a plugin author, the result would not be too different, boilerplate-wise. However, housekeeping would be dramatically different. Instead of performing operations to add or configure functionality, plugins would declare functionality and the framework would be responsible the housekeeping.

The filesystem can be used as boilerplate in a declarative system. That is, by simply being in a specific directory, a file may be defining what it is for or how it should be utilized. This is common with MVC frameworks, where files within a `controllers` directory imply `filename.ext` should handle a request to `/filename` or files within a `models` directory inform the framework to create tables or collections in a database where the table or collection name is the same as the file name.

By composing an object or positioning files in a hierarchy, a plugin simply describes how something is supposed to be. The housekeeping—how to add, update, or remove functionality, and in what order—is left up to the framework.

In my primitive proof-of-concept, I took very naive approaches for each part of the system?—?configuration, event handling, web service route definitions. I also made decisions based on language features in Javascript, like the deep merging of nested objects.

For configuration, a plugin may be designed to have an extensible list by storing a default value in an object `{stuff: {default: ’Default’}}` such that a separate, independent plugin could add an item to that list by specifying `{stuff: {newItem: ’New Item’}}` and allowing the framework to construct the final list by merging the declarations into `{stuff: {default: ’Default’, newItem: ’New Item’}}`. If sort order is important, they could be defined as a more robust object, like `{stuff: {default: {value: ‘default’, display: ‘Default’, sort: 1}}}`.

Drawbacks

The way I designed my architecture, a plugin would synchronously return an object with its configuration. For flexibility, I added an asynchronous initialization function that, if provided, would be called after all configurations had been loaded. The most obvious need for something like this was for configuration that was dependent on an asynchronous resource, such as a database call. A different way to address that problem would be to provide a way for a database query to be expressed in the synchronous result and rely on the framework to trigger the database query and embed the results. Ultimately, I wanted to support other types of asynchronous configuration, and wanted the framework to be decoupled from knowledge of a database (which is itself provided by a plugin).

With a blog, you may have multiple themes installed, and thus selectable in a list, but you will want to store the currently selected theme in a persistent data store. This means a plugin cannot declare “the current blog theme is ME.” This is not necessarily a problem with a declarative design, so much as it’s an age-old challenge with the combination of synchronous and asynchronous systems. Static declaration can only get you so far before you find yourself needing dynamic configuration.

I also worry that by doing things in a different way may alienate plugin authors who are more accustomed to imperative systems, even if they necessitate more complex housekeeping.

Ultimately, I’m very happy with the reduction of complexity I have been able to achieve through this shift in thinking from imperative to declarative designs.

I am curious if there is any literature on the design of extensible systems. There are pros and cons to each method, but perhaps people have discovered clever ways of getting around drawbacks in the declarative approach.

Leave a Reply

Your email address will not be published. Required fields are marked *