Home Design Patterns Extensibility
Extensibility
FoundationsProvide a framework of modules that allows functionality to be added at a later date.
Purpose
Inheritance provides a very basic mechanism for extensibility. The extensibility pattern expands on this by dividing the project into modules.
The goal is that the core stops changing. New behavior arrives as a new module rather than as a new branch in code that already works, so the part of the system you cannot afford to break is not touched by the twentieth feature. That idea is old and has a name of its own, the open/closed principle: open to extension, closed to modification. What makes it a pattern rather than a slogan is the machinery underneath, which is an interface the core publishes, a registry the core owns, and a decision about when modules get bound.
Design
There are several ways to accomplish the extensibility pattern. You can create modules that are either instantiated by another module or included as part of an abstract class. What the choices really differ in is binding time. A module can be bound at compile time, linked in with the rest of the image, which costs nothing at run time and means adding one is a rebuild. It can be bound at load time, discovered by scanning a directory or a configuration file, which is what most plug-in systems do. Or it can be bound at run time by a script, which is the most flexible and the hardest to keep fast and safe.
Only two things have to be designed carefully. The first is the interface: it needs to be narrow enough that somebody can implement it correctly without reading your source, and expressive enough to be worth implementing. The second is the registration point, which is where a module announces itself and the core decides whether to accept it. Everything else, discovery, ordering, isolation and versioning, follows from those two.
Example
The Autobot Robotics vision system inspects parts for defects, and every customer's idea of a defect is different. A food plant cares about foreign material, an aerospace shop cares about surface finish on a specific bracket, and neither cares about the other. For the first few customers those rules went straight into the inspection code, guarded by a switch on the site identifier.
By the sixth customer that switch was the most edited file in the repository, and it had become genuinely expensive. Every change for one plant meant the whole build had to be re-qualified for all of them, because it was the same binary, and re-qualifying meant a release cycle measured in weeks. The shipping firmware carried every customer's rules including several for customers who had gone. One plant's request for a threshold change was quoted as a month of work, and that quote was honest.
The rules became modules. The core defines an inspection interface, a registry loads the modules named in the site configuration file, and the inspection loop calls whatever is registered. One could also only load the modules that were needed for the specific task, which is what the scheduler does on the smaller controllers where memory is tight. The core has no customer names in it any more, and a threshold change ships as one module without re-qualifying the rest. The cost, which we did not see coming, was that the interface immediately became a published contract: two integrators wrote modules against it, and it has been frozen ever since. A module that threw an exception also used to take the vision pipeline down with it, so module execution was moved off the real-time path and given a time budget it is killed for exceeding.
interface Module
{
string name();
bool handles(Frame f);
Defect inspect(Frame f);
}
class Core
{
private List<Module> modules = new List<Module>();
public void register(Module m) {
// reject duplicates, check the module's declared version
modules.Add(m);
}
public void run(Frame f) {
foreach (Module m in modules) {
if (m.handles(f)) {
// call inspect() with a time budget and catch anything it throws
}
}
}
}
class ForeignMaterial : Module // shipped with the food plant
{
public string name() { return "foreign-material"; }
public bool handles(Frame f) { }
public Defect inspect(Frame f) { }
}
class SurfaceFinish : Module { } // shipped with the aerospace site
class Loader
{
public void load(Core core, SiteConfig cfg) {
// instantiate the module named by each entry and register it
}
}
Core mentions Module and never a concrete
class. The only place a module's name appears is the site configuration file
that Loader reads.Seen in the Wild
The Linux kernel loads drivers as modules with insmod and
modprobe, against an interface that is deliberately not stable
across versions, which is a real design position with real consequences for
anybody shipping a driver out of tree. Apache httpd is built the same way, with
mod_ssl and mod_rewrite brought in by
LoadModule lines in the configuration. Eclipse is the extreme case:
the IDE is a small OSGi runtime and everything else, including the Java editor,
is a plug-in. On the library side, Java's ServiceLoader finds
implementations of an interface at run time from the class path, and PostgreSQL
extensions such as PostGIS add data types, operators and index support to a
running server with CREATE EXTENSION.
Comparisons
- Strategy is the smallest possible version of this: one operation made replaceable. An extension frame is the same idea applied to a whole feature and given a registry.
- Template Method is the basic inheritance mechanism the first line of this page refers to. The base class fixes the order and leaves holes, and the holes are the extension points.
- Observer is how modules usually get told that something happened without the core knowing who is listening.
- Abstract Factory is a common way to let a module supply a family of objects rather than a single one.
- Module is about the narrow public surface of one unit of code. This pattern is about a core that can accept units it has never heard of.
- Pipes and Filters is an extension frame where the interface happens to be "take this data and give me some back", and stages are the plug-ins.
Why It Exists
My take
The honest reason this exists is release engineering, not design. The core is the part you cannot afford to break, and every feature that lands in it costs a full regression run. In embedded work it costs more than that: a firmware release, a validation pass, and for some customers an engineer driving to a plant. A module boundary is a way of buying the right to ship a change without re-qualifying everything, and that is a commercial argument that happens to have an architectural shape. When people cannot explain why their plug-in system was worth it, this is usually the answer they are reaching for.
The second reason is organisational. An extension point lets somebody who is not on your team, does not have your source, and cannot get a slot in your release schedule add behavior to your system. Every successful plug-in architecture I can think of was built for that reason rather than for internal tidiness. It is also the reason the costs are so easy to underestimate: the moment a second party writes a module, your interface is a published contract and you are in the compatibility business permanently.
My reservation is the one I have had since I first wrote this page. "Extensibility" describes a goal, not a mechanism. The things that actually get you there are Strategy, Observer, Template Method, a registry and some form of dynamic loading, and knowing those five is more useful than knowing this name. What survives as advice is narrower and I would still defend it: put the variable part behind an interface, keep the registry in the core, and add extension points when the second customer asks rather than the first.
Criticisms
The extensibility pattern is not very well defined. It is close to a basic design principle, but several sources separate it out as a specific pattern. That vagueness is the first criticism: two teams can both say they are doing it and mean an abstract base class in one case and a dynamically loaded binary in the other, and those have almost nothing in common except an intention.
Then there is speculative generality, which is the usual way this goes wrong. Extension points that nobody ever extends are pure cost: an interface with one implementation, a registry with one entry, indirection in the middle of the hot path, and a reader who has to chase two files to find out what happens next. Guessing which axis of the system will need to vary is difficult, and when you guess wrong the frame is worse than useless, because the actual variation arrives somewhere the frame does not reach and you end up editing the core anyway with an unused plug-in system still in the way.
Loading third-party code into your process is also a decision people make without noticing they have made it. A module runs at your privilege level, in your address space, with your priorities. On a real-time controller a module that blocks is a missed deadline and a stopped machine, and no amount of interface design prevents that; only a process boundary or a time budget does. And once enough behavior lives in modules the system's actual behavior is described by a configuration file rather than by any call graph, which is the same debugging problem a deep Decorator stack has. The structure is real, it just does not exist anywhere in the source.