Composability and Inversion of Control

Composable code is often an aim and effect of Inversion of Control (IoC) strategies. Techniques such as dependency injection work with self-contained components that are passed (“injected”) into the places where they’re needed. This is an example of IoC – the outer environment is responsible for resolving the dependencies of the deeper code layers it calls.

The concept of composability encapsulates the specific pieces you can provide and how they’re integrated together. A composable system will consist of distinct units of functionality that each have a single responsibility. More complex processes are developed by “composing” several of these units into a new larger one.

Examples of Composability

Here’s an example of three possible functional units:

Now let’s add a logger implementation into the mix:

Let’s now consider another type of log message:

Here we’re seeing the benefits of composability. By defining the application’s functionality as interfaces, concrete class implementations are free to mix and match the pieces they need. Not every log message has an associated user; some messages might be ineligible for email alerts if they’re low-priority or contain sensitive information. Keeping the interfaces self-contained lets you create flexible implementations for each situation.

The examples above are written in PHP but could be replicated in any object-oriented language. Composability’s not limited to OOP code though: it’s also a foundational aspect of functional programming. Here complex behaviors are obtained by chaining small functions together. Functions may take other functions as arguments and return a new higher-order function as a result.

This minimal JavaScript example uses the compose-function library to compose the square and quadruple units into another function that squares and then quadruples its input. The compose() utility function accepts other functions to compose together; it returns a new function that calls the chain of inputs in sequence.

You’ll also come across composability in modern componentized development frameworks. Here’s an example of a simple set of React components:

Each component is kept simple by only concerning itself with a specific part of the overall functionality. You can render the UserCard on its own or compose a new variant with an avatar or any other React component. The UserCard isn’t complicated by the logic responsible for rendering the correct avatar image file.

Composition vs Inheritance

Object-oriented languages often achieve code reuse through the means of inheritance. Choosing inheritance as your default strategy can be a costly mistake that makes it harder to maintain a project over time.

Most languages don’t support multiple inheritance so your options for complex combinations of functionality are limited. Here’s the log message from earlier refactored into a similar inheritance model:

These classes might seem helpful to begin with. Now you don’t need specific log message implementations like our UserLoggedInMessage class. There’s one big problem though: if you need to write a log message which relates to a user and sends an email, there’s no class for that. You could write a LogMessageWithEmailAndUser but you’d be starting down the slippery slope of covering every possible permutation with “generic” concrete class implementations.

Despite these issues, code using inheritance for this kind of relationship model remains prevalent in projects large and small. It does have valid use cases but is often implemented for the wrong reasons. Small composable units based on interfaces and functions are more versatile, make you think about the bigger picture within your system, and tend to create more maintainable code with fewer side-effects.

A good rule of thumb for inheritance is to use it when an object is something else. Composition is usually the better choice when an object has something else:

Log / Email – A log message is not inherently an email but it may have email content associated with it. The Log should include the Email content as a dependency. If not all Logs will have an Email component, composition should be used as shown above. User / Administrator – The Administrator inherits all the behaviors of the User and adds a few new ones. It could be a good case for inheritance – Administrator extends User.

Reaching for inheritance too early can restrict you later on as you find more unique scenarios within your application. Keeping units of functionality as small as possible, defining them as interfaces, and creating concrete classes that mix and match those interfaces is a more efficient way to conceptualize complex systems. It makes your components easier to reuse in disparate locations.

Summary

Composable code refers to source that combines self-contained modular units into bigger chunks of functionality. It’s an embodiment of “has-a” relationships between different entities. The actual composition mechanism depends on the paradigm you’re using; for OOP languages, you should program to interfaces rather than concrete classes, whereas functional realms often guide you towards good composability by design.

Being proactive in your use of composable techniques leads to more resilient code that’s loosely coupled, easier to reason about, and more adaptable to future use cases. Creating composable blocks is often the most effective starting point when you’re refactoring large-scale systems. Although alternatives like inheritance have valid roles too, they’re less broadly applicable and more prone to misuse than composability, dependency injection, and IoC.