Home Design Patterns Extension Object

Extension Object

Modern Practice

Let clients ask an object at runtime for an interface it might also support.

Purpose

An Extension Object keeps the base interface small and moves optional capabilities into separate objects that a client can ask for by name. The base type grows one operation, something like getExtension(key), and that operation returns either an object implementing the requested capability or nothing at all. The object answers for itself, so the client never has to know which concrete class it is holding.

The situation it is built for is narrow and real: an interface that has already shipped, implementations that have gained abilities the interface does not describe, and clients you cannot recompile. Widening the base interface makes every implementation carry a method it cannot honor. Downcasting to a concrete class puts a model number in the caller. Extension Object is the third option: make capability a question rather than a declaration.

Design

The subject holds a table of extensions and hands them out by key. Each extension is a normal object with a normal interface, so once a client has one it programs against it in the ordinary way. The interesting design choices are what the key is and what "no" looks like. A string key is flexible and typo-prone; a type token is safer and still not checked by the compiler. "No" is usually a null return, occasionally an error code, and it must be something the client is forced to look at.

UML class diagram: a Subject interface with getExtension, an Arm1210 holding a map of extensions, and two concrete extensions under an Extension interface.
Everything hangs off one operation. The dependency from Subject to Extension is a return type, not a call, and that is why the base interface does not grow when a new capability arrives.

Extensions usually hold a back-reference to their subject, because a capability implemented as a separate object still needs to act on the thing it belongs to. That back-reference is what stops the extension being a free-floating helper and makes it part of the object's identity.

Example

Autobot Robotics's task scheduler drives every arm through one IArm interface: move, home, report status, stop. The three arm models have been in the field for years and the interface is compiled into customer integrations that Autobot does not control.

Then force sensing shipped, on the Arm1210 only. Adding readForce() to IArm would have forced the 601 and the 802 to implement a method that throws, which turns a capability question into an exception handler in every caller. Someone had already started down the other road, and there was a line in the scheduler that read if (arm is Arm1210). That line is the reason the team went looking for a pattern: it put a model number into scheduling logic, and it would need a sibling every time a model gained anything.

They added getExtension to IArm once. The scheduler asks for force sensing, uses it if it comes back, and runs the ordinary path if it does not. When pallet calibration arrived a year later, on a firmware revision rather than a model, it was a new extension and no change to IArm at all. The cost showed up on the same day it shipped: a mistyped key in a test harness returned null, the test's force-sensing branch quietly did not run, and the harness passed. The compiler had nothing to say about it, and that is the bargain this pattern makes.

interface IArm
{
    void moveTo(Pose p);
    object getExtension(string key);   // the whole pattern
}

interface IForceSensing
{
    double readForce();
}

class Arm1210 : IArm
{
    private Dictionary<string, object> exts = new Dictionary<string, object>();

    public Arm1210()
    {
        exts["force"] = new ForceSensor(this);   // extension knows its subject
    }

    public object getExtension(string key)
    {
        return exts.ContainsKey(key) ? exts[key] : null;
    }

    public void moveTo(Pose p) { /* some code */ }
}

class Arm601 : IArm
{
    // Older hardware. It supports nothing optional, and says so.
    public object getExtension(string key) { return null; }
    public void moveTo(Pose p) { /* some code */ }
}

class Scheduler
{
    public void runStep(IArm arm)
    {
        IForceSensing f = arm.getExtension("force") as IForceSensing;
        if (f != null)
        {
            // some code, using f.readForce()
        }
        // No model numbers in here. The arm answers for itself.
    }
}
The two lines that matter are the getExtension("force") call and the cast beside it. Everything the pattern gives you and everything it costs is in that one expression.

Seen in the Wild

The best real anchor is COM. Every COM object implements IUnknown, whose QueryInterface takes an interface identifier and returns either a pointer to that interface or E_NOINTERFACE. That is Extension Object promoted to an operating system ABI, and OLE, ActiveX, DirectX and the Windows Shell are all built on it. Mozilla's XPCOM copied the idea, QueryInterface and all. In the Java world, Eclipse's IAdaptable.getAdapter(Class) is the same mechanism with a type token instead of a GUID, and the platform's adapter registry exists so one plugin can add a capability to another plugin's objects. .NET's IServiceProvider.GetService(Type) has the same shape and the same null-means-no convention.

Comparisons

  • Visitor also adds operations to a hierarchy from outside it, but it needs every concrete type known up front and it breaks when one is added. Extension Object survives new types and new capabilities arriving separately, at the cost of the compile-time checking Visitor keeps.
  • Decorator adds behavior while preserving the interface, so the client cannot tell anything changed. Extension Object adds a new interface and requires the client to go and ask for it.
  • Adapter converts an interface you were handed into one you wanted. Extension Object hands you an interface the object already implemented and was not advertising.
  • Marker Interface answers a very similar question at compile time instead, about a type rather than an instance. If your capabilities are fixed per class and you can recompile the callers, prefer the marker: the compiler does the work.
  • Abstract Factory is the usual way the extension table gets populated when the set of extensions varies by configuration or firmware level.

Why It Exists

My take

This pattern exists because of published interfaces. Inside one codebase the problem it solves does not really exist: if implementations gained a capability, you widen the interface, fix the compile errors, and move on. That option disappears the moment the interface has crossed a boundary you do not control, which is exactly what happens to an SDK, a plugin API, or an OS-level ABI. Then "add a method" means "break every caller in the world", and the only extensibility left is the kind that can be negotiated at runtime.

COM is the clearest evidence that this is a real pressure and not a theoretical one. Microsoft had to let a shipped binary interface grow for decades across independently compiled components, and the answer they arrived at was that an object gets asked what it supports and answers honestly. That decision propagated into a generation of Windows software. It is worth noticing that they did not treat it as free: the whole apparatus of interface identifiers, reference counting and identity rules exists to make the question safe to ask repeatedly, and getting those rules right was a career skill.

The cost is one sentence long and worth saying plainly: it moves a type error from compile time to run time. The compiler cannot warn you that a scheduler asking for force sensing will never get it from an Arm601, cannot tell you when you have spelled the key wrong, and cannot find the callers when an extension is withdrawn. You have traded a guarantee for flexibility. That is a fine trade at an ABI boundary where the guarantee was unavailable anyway. Inside a single system where you could just recompile, it is a bad trade dressed up as architecture, and I have seen more of the second than the first.

Criticisms

The runtime failure mode is quiet, which is the worst kind. A client asks for a capability, gets null, and takes the other branch. If it asked for the right thing and the object genuinely does not support it, that is correct behavior. If it asked for the wrong thing, it is a bug, and the code path is indistinguishable. Nothing throws, no log line appears, and the feature simply does not happen. A string key makes it worse, and even a type token only narrows the mistake rather than eliminating it.

There is also a discoverability problem that no amount of care fixes. Nothing in the type of an object tells you what extensions it might have. The set is not in the interface, not in the class declaration, and often not in one place at all, since a well-built extension mechanism lets extensions be registered from elsewhere. Documentation becomes the API, with all the accuracy that implies. Any tool that works from types alone, which includes autocomplete, refactoring, dead code analysis and most static checkers, cannot see across the seam.

Finally, it scales badly with the number of capabilities. Two optional features read fine. Twelve produce a caller full of ask-and-test blocks, each with a fallback path that may or may not be exercised in any given deployment, which is a combinatorial testing problem you have created for yourself. At that point the honest question is whether the base interface was factored wrongly, and quite often the answer is yes and the right move is to split the type rather than to keep bolting capabilities onto one.