Unlike many apps, “Pluggable” apps can be extended with self-contained pockets of code called “Plugins”.

These apps tend to be modular by design, resulting in manageable, loosely-coupled code.

Today, let’s learn how to build pluggable apps.

Every day, you probably use pluggable apps:

Alt Text

Your favorite development tools are probably pluggable too:

Alt Text

The problem is, there are too many problems.

With Plugins, you dont need to solve every problem. Users solve their own problems.

When you build a feature as a Plugin, the logic is centralized instead of being spread throughout the codebase. This modularizes the feature, keeping it loosely-coupled with its dependencies.

When you build your entire app as a Tree of Plugins, those benefits extend to the entire codebase. Ultimately, this benefits you, your team, and your customers.



Building Non-Pluggable Systems

Imagine you’re a duck named Lenny (), and you love to quack. Most of your friends love to quack too, except Lonnie ().

Anyways… you live in a park and people like to throw food at you (despite the litany of signs indicating not to).

One day, you notice youve become quite plump. So, you build a web service to track your consumption:

//  food-service.ts

//  Log of Foods Eaten
//  Example:  [{ name: "lenny", food: "waffle", calories: 5 }]
const foods = [];

//  Function to Log a Food (by Duck Name)
const logFood = (name: string, food: string, calories: number, ...props: any) => {
  foods.push({ name, food, calories, ...props });
}

//  Function to Get Log (by Duck Name)
const getLog = (name: string) => {
  return foods.filter(food => food.name === name);
} 

//  JS Module Exports
export logFood, getLog;
Enter fullscreen modeExit fullscreen mode

Congratulations, tracking has given you the bill-power to lose 3 ounces!

That’s great, but your friend Mack () has no self control. So, he asks you to scare the humans with a horn once he exceeds his 300 calorie daily limit.

Then your friend Jack () asks if you can also track protein. Hes already fit, so shes more concerned with staying jacked than losing fat.

Before you know it, Abby (), Tabby() and Doug () are asking for features. Even Larry () wants something, and you’re pretty sure he’s the one who ate Lonnie ()!

The whole pond descends upon you, the backlog is full, and now the app is so complex that you’re losing customers talking about “the good old days” when things were simple.

Alt Text

Then you wake up… “Are you ok honey?”, asks your wife Clara () as she waddles in with a basket of breadcrumbs.

You: “I had the nightmare again…”

Clara turns to you, chuckles and says:

Silly Goose! The pain of feature creep, non-modular code, and tightly coupled design can be largely avoided with a plugin-oriented architecture (POA).

Looking up to meet her gaze you say, “You’re right dear. let’s recap the basics of plugin oriented design so we never forget.”

With a warm embrace Clara replies, “I can’t think of a better way to spend our Sunday =)”



Building Pluggable Systems

A fundamental characteristic of a plugin-oriented architecture is the ability to alter functionality without altering the existing system definition.

So, to make your Food Service “pluggable”, you decide to do two things:

  1. Register: Allow users to register custom functions.
  2. Invoke: Run the registered functions when a condition is met.

With this, other developers can inject functionality into your app.

These registration points are called Hooks.

We see this pattern everywhere:

I recommend taking a look at tapable. This is the small module underlying every Webpack Plugin.

Here’s the Food Service code updated to use Hooks:

//  pluggable-food-service.ts

//
//  Define the Hook
//

type LogFoodFunction = (name: string, food: string, calories: string, ...props: any) => void;

//  List of Functions Registered to this "Hook"
const functions: LogFoodFunction[] = [];

//  Add a Function to the Hook
const addFunction = (func: LogFoodFunction) => {
  functions.push(func);
}

//
//  Build the Food Service
//

//  List of Foods Eaten
//  Example:  [{ name: "lenny", food: "bread", calories: 5 }]
const foods = [];

//  Add the Core Function
addFunction((name, food, calories) => {
  foods.push({ name, food, calories });
});

//  Function to Log a Food (by Duck Name)
const logFood = (name: string, food: string, calories: number, ...props: any) => {
  //  Trigger Functions in the Register
  functions.forEach(func => func(name, food, calories, ...props));
}

//  Function to Get Log (by Duck Name)
const getLog = (name: string) => {
  return foods.filter(food => food.name === name);
} 

//  JS Module Exports
export logFood, getLog, addFunction;
Enter fullscreen modeExit fullscreen mode

Now, anyone can extend this JS Module by calling addFunction.

Heres Mackss () Plugin to scare humans with a horn:

//  macks-plugin.ts
import * as FoodService from "pluggable-food-service";
import * as Horn from 'horn-service';

//  Set Calorie Limit
const calorieLimit = 300;

FoodService.addFunction(() => {

  //  Get Total Calories
  const eatenCalories = FoodService.getLog("mack").reduce((prev, entry) => prev + entry.calories);

  //  Check Condition
  if (eatenCalories > calorieLimit) { Horn.blow() }
})
Enter fullscreen modeExit fullscreen mode

Now, all you have to do is import Mack’s Plugin, and the feature will be integrated.

This Hook Pattern is just one way to build a Plugin Oriented Architecture.



Hook Alternatives

Hooks (and their variants) are fairly common. Probably because they’re simple:

Build a way to register code, and invoke the code when a condition is met.

But, they’re not the only way to build a pluggable system.



Primitive Domain

In the code above, we register “primitive” code with a Hook. Fundamentally, primitive code is just one way to encode intent. In this case, it’s then decoded by the JS runtime.



Application Domain

However, intent can be encoded in other ways too. For example, you can build your own language. It sounds complicated, but it’s exactly what you do when you define classes or build an API. Your application logic is then responsible for managing and decoding entities in this domain.



External Domain

In some cases, you may want to externalize the entire process. For example, you can trigger external code with Webhooks, Websockets, and tools like IFTTT, Zapier, and Shortcuts.

Regardless of the implementation, it helps to remember this golden principle:

Keep it simple.

a.k.a. don’t do more than reasonably necessary

This applies to you, your team, your functions, modules, app, and everything you touch. If something is too complex, try to break it up. Refactor, rework, and fundamentalize as necessary.

A plugin oriented architecture (POA) can help achieve this goal, especially as logic becomes complex. By modeling each feature as a Plugin, complexity only bubbles up when necessary, and in a predictable, modularized container.



Hook Concerns

There are several concerns with the hook implementation we built above:

  • Centrality: You’re responsible for loading Plugins.
  • Trust: You’re responsible for auditing code.
  • Conflicts: Users may disagree on the feature set.
  • Dependencies: No management system for complex dependencies.
  • More: A whole lot more.

These concerns can be addressed using various strategies:

  • Dynamic Injection: Dynamically inject code from an external resource (like a URL) at runtime.
  • Contextual Activation: Activate features based on the current context (logged in users, application state, etc…)
  • Plugin Managers: Coordinates feature extension, even in a complex network of dependencies.
  • More: A whole lot more.

I hope to cover “Dynamic Injection” and “Contextual Activation” in a future post.

For now, let’s look at “Plugin Managers”.



Plugin Managers

It can be challenging to say organized when you’re dynamically importing code that changes other code.

If you build a new feature that depends upon another feature (or multiple), you’d likely end up “splitting and spreading” the feature across the app.

We built “Halia” to help solve this problem:

Halia – Extensible TS / JS Dependency Injection Framework

Halia Logo

Use Halia to build your app as a “Tree of Plugins”. Your app’s core is packaged as a Plugin, and your features are packaged as Plugins. Then, each Plugin can be extended by additional Plugins.

Halia accomplishes this using “Dependency Injection”. For more info on Dependency Injection Frameworks (like Angular, Nest, and Halia) see our article:

Dependency Injection with Doug the Goldfish

Doug Image



Conclusion

The concepts discussed here are just the start. We’ve opened a can of worms, but for now, let’s put the worms back in the can. We’ve already overfed the park animals.

Speaking of which, we found Lonnie ()! It turns out she was just across the pond learning plugin-oriented architecture (like all good ducks do).

In closing, there are plenty of ways to cook your goose, so you might as well be a duck ().

Cheers,
CR

LEAVE A REPLY

Please enter your comment!
Please enter your name here