Build Extensible Apps with Lenny the Duck 🦆
Tags: javascript, programming, webdev, typescript
Published: 2021-01-31 18:10:19

Unlike many apps, "Extensible" 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 extensible apps.

# Introduction

Every day, you probably use extensible apps:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/bo7kfomrpsoxdgyj00v5.png)

Your favorite development tools are probably extensible too:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/f2sy21072ld1qhg2h8my.png)

The problem is, there are too many problems.

>  With Plugins, you don’t need to solve every problem.  Users solve their own problems.

With plugins, feature logic can be centralized instead of spread throughout the codebase.  This leads to modularized, loosely-coupled features.

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

## Building Non-Extensible 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 you’ve become quite plump.  So, you build a web service to track your consumption:

```typescript
//  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;
```

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.  He’s already fit, so he’s 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](https://dev-to-uploads.s3.amazonaws.com/i/vmzb4pfet05dl816inlv.jpg)

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

"I had the nightmare again...", you reply in an anxious tone.

“Silly goose”, Clara chuckles and says: 

> The pain of feature creep, non-modular code, and tightly coupled functionality can be largely avoided with plugin-oriented design (POD).

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 Extensible Systems

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

So, to make your Food Service "extensible", 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](https://github.com/webpack/tapable).

We see this pattern everywhere:

- **Wordpress**:  ["Filters"](https://github.com/WordPress/WordPress/blob/37662df05e948eaf77b17e6fd27d519987f179db/wp-includes/class-wp-hook.php#L73) and ["Actions (*gasp* it's just another Filter)"](https://github.com/WordPress/WordPress/blob/37662df05e948eaf77b17e6fd27d519987f179db/wp-includes/plugin.php#L409)
- **Babel**: [Visitor Plugin Function](https://github.com/babel/babel/blob/main/packages/babel-core/src/config/plugin.js#L23)
- **Webpack**:  [Tapable](https://github.com/webpack/tapable)

>  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:

```typescript
//  extensible-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;
```

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

Here’s Macks’s (🐦) Plugin to scare humans with a horn:

```typescript
//  macks-plugin.ts
import * as FoodService from "extensible-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() }
})
```

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

However, building a system with “Hooks” is just one way to implement “POD” principles.

## 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 an extensible system.

#### Primitive Domain
In the code above, we register "primitive" code with a Hook.  Fundamentally, primitive code is just an *encoding* of 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. 

Plugin-Oriented Design (POD) 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:

-  **External Plugins**:  Dynamically inject code from an external resource (like a URL) at runtime.
-  **Contextual Activation**:  Dynamically 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 "External Plugins", "Contextual Activation", and related topics in future articles.

To learn about "Plugin Managers" and how our new tool "Halia" can help you build Extensible JS / TS systems, see our blog post:

[Plugin Oriented Design with Halia](https://dev.to/codalreef/building-apps-as-a-tree-of-plugins-with-halia-1e5o)


##  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

*For more articles like this, follow me on:  [Github](https://github.com/CodalReef), [Dev](https://dev.to/codalreef), [Twitter](https://twitter.com/CodalReef), [Reddit](https://www.reddit.com/user/CodalReef)*