DEV Community

Cover image for Build Extensible Apps with Lenny the Duck 🦆
Oranda
Oranda

Posted on • Edited on

Build Extensible Apps with Lenny the Duck 🦆

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

Your favorite development tools are probably extensible too:

Alt Text

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:

//  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 mode Exit 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. 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

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.

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:

//  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;
Enter fullscreen mode Exit fullscreen mode

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

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

//  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() }
})
Enter fullscreen mode Exit fullscreen mode

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

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, Dev, Twitter, Reddit

Top comments (5)

Collapse
 
riviergrullon profile image
Rivier Grullon

Ikari-goose Interesting, Amazing post ❤️

Collapse
 
zuluana profile image
Oranda

Thanks for taking a look Rivier =)

I just checked out your Deno package manager, awesome!

Collapse
 
zuluana profile image
Oranda • Edited

Please note that I republished this article to update the URL. If there's a way to change the URL without reposting, please let me know!

Collapse
 
ben profile image
Ben Halpern

Great post!

Collapse
 
zuluana profile image
Oranda • Edited

Thanks Ben! I’m glad you enjoyed the post =)

Btw, I’m new to Dev.to, and I’m loving the platform. Just curious, does Forem have plugin support?