Comb Inheritance

Comb Inheritance

This article is a re-posting of the original located at: https://thenewobjective.com/types-and-programming-languages/comb-inheritance/

Introduction

Peter Norvig has famously said that "Design patterns are bug reports against your programming language". A popular design pattern still in use today is the chain of responsibility pattern, which allows an object to pass a request along a chain of potential handlers who can process and/or pass the request further along the chain. The key goal here being that the sender of the request does not need to know which handler will ultimately process the request, enabling loose coupling between the sender and the handlers.

Despite the loose-coupling benefits, to implement this pattern in a traditional object-oriented programming language often requires a fair amount of boilerplate code to set up the chain of handlers and the logic for passing requests along the chain. However, in JavaScript, we can leverage its prototypal inheritance model plus Proxies to enable a more elegant solution known as comb inheritance.

The Chain of Responsibility Pattern

Objects are intended to be self-contained units of behavior and state. However, in many scenarios, an object may not be able to handle a request on its own. For example, consider a logging system where different loggers handle messages of varying severity levels. In these cases, the chain of responsibility pattern allows an object to delegate the request to the next object in a chain until one of the objects can handle it.

Here is how a chain of responsibility is structured:

Article content

Sequentially, when a Sender sends a Request, it invokes the handle method on its associated Handler. If the Handler can process the Request, it does so and returns a Response. If not, it forwards the Request to its nextHandler.

Article content

Logger Example

As a practical example, consider a logging system with three handlers: InfoHandler, WarningHandler, and ErrorHandler. Each handler is responsible for processing messages of a specific severity level. If a handler cannot handle a message, it passes the message to the next handler in the chain. The Logger acts as the Sender and exposes a natural log() action.

Article content

Comb Inheritance

The Logger example above illustrates the chain of responsibility concept well, but notice how much boilerplate is required: abstract base classes, explicit handle() methods, canHandle() and process() abstractions, and manual chain setup. What if we could make this pattern more implicit and natural?

Enter Comb Inheritance, a form of multiple inheritance pioneered by NewtonScript, the programming language developed for Apple's Newton platform in the 1990s. The name "comb" comes from the visual appearance when you diagram the inheritance structure - it looks like a comb with two distinct paths (or "teeth"):

  1. The prototype chain (_proto) - Traditional prototype-based inheritance for shared behavior and default values
  2. The parent chain (_parent) - Instance-based delegation for context-specific behavior

Here's how the two paths work together:

Article content

When looking up a property on Child:

  1. Check Child's own properties
  2. Walk up the prototype chain: Child.prototype → Object.prototype
  3. If not found, delegate to Parent instance and repeat from step 1
  4. If still not found, delegate to GrandParent instance and repeat from step 1

This creates a powerful composition pattern where objects can inherit both from type hierarchies (prototypes) and from contextual relationships (parents).

Implementing Comb Inheritance in JavaScript

JavaScript's prototypal inheritance naturally handles the proto path, so we only need to add the parent path. We can achieve this simply enough using a Proxy to intercept property lookups:

Article content

What's notable in this implementation is that accessing parent[prop] automatically re-enters the proxy's get trap, handling the entire parent chain recursively without explicit loops.

Revisiting the Logger Example

With Comb Inheritance, our logger chain becomes dramatically simpler:

Article content

Notice how we've eliminated:

  • The abstract Handler base class
  • The handle(), canHandle(), and process() methods
  • The Sender wrapper class
  • All the boilerplate for managing the next handler

The chain of responsibility is now implicit in the inheritance structure itself. When super.log() is called and the method doesn't exist on the prototype, the Proxy intercepts and delegates to the parent instance, seamlessly implementing the pattern.

Document Hierarchies

Comb Inheritance shines in scenarios where objects need both type-based and context-based behavior:

Consider a document system where pages inherit both formatting rules from their template class (prototype chain) and specific overrides from their container hierarchy (parent chain):

Article content

This example showcases both "teeth" of the comb:

  • Prototype chain (horizontal): page42 → PageTemplate.prototype → DocTemplate.prototype → Component.prototype

Provides leftMargin, rightMargin, bottomMargin (default 1.0), paperSize (default 'letter'), and getMargins() method

  • Parent chain (vertical): page42 → chapter3 → techManual

Provides headerText and footerText from specific document instances

Event Propagation

A UI framework where events naturally bubble up the component hierarchy without explicit listener registration:

Article content

The event system leverages both inheritance paths:

Prototype chain: handleEvent() method is shared across all UIComponent instances via prototype

Parent chain: Events automatically propagate up the component hierarchy (saveButton sidebar mainWindow)

This eliminates the need for explicit event listener registration, event emitters, or manual propagation logic. The UI hierarchy itself defines the event propagation path.

Conclusion

Comb Inheritance demonstrates how we can eliminate the need for a number of Object-Oriented design patterns as separate constructs. By making the chain of responsibility implicit in the inheritance mechanism itself, we reduce boilerplate, improve clarity, and make our code more maintainable.

While NewtonScript introduced this concept in the 1990s, JavaScript's dynamic nature and Proxy support make it a possible to reintroduce this for modern applications. As we've seen through the logger, document hierarchy, and event propagation examples, this pattern naturally fits scenarios where objects need both type-based behavior (via prototypes) and context-specific delegation (via parent instances).

The implementation is available on NPM at @mlhaufe/comb-inheritance, complete with tests for all examples presented in this article.

References and Further Reading


To view or add a comment, sign in

More articles by Michael Haufe

Explore content categories