Photo of colourful woodblocks
Shutterstock.com/locrifa

Composable code describes classes and functions that can be readily combined to create more powerful higher-level constructs. Composability compares favorably to alternative forms of code reuse such as object-oriented inheritance. It advocates the creation of small self-contained units that are treated as building blocks for bigger systems.

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:

interface LogMessage {
    public function getMessage() : string;
}
 
interface Mailable {
    public function getEmailContent() : string;
}
 
interface RelatesToUser {
    public function getTargetUserId() : int;
}

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

interface Logger {
    public function log(LogMessage $message) : void;
}
 
final class SystemErrorLogMessage implements LogMessage, Mailable {
 
    public function __construct(
        protected readonly Exception $e) {}
 
    public function getMessage() : string {
        return "Unhandled error: " . $this -> e -> getMessage();
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
}

Let’s now consider another type of log message:

final class UserLoggedInLogMessage implements LogMessage, Mailable, RelatesToUser {
 
    public function __construct(
        protected readonly int $UserId) {}
 
    public function getMessage() : string {
        return "User {$this -> UserId} logged in!";
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
    public function getTargetUserId() : int {
        return $this -> UserId;
    }
 
}

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.

const square = x => (x * x);
const quadruple = x => (x * 4);
 
// 16
console.log(compose(square, quadruple)(2));
Advertisement

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:

const UserCard = ({user, children}) => (
    <div>
        <h1>{user.name}</h1>
        {children}
    </div>
);
 
const UserAvatar = ({user}) => {
    if (user.avatarId) {
        return <img src={`/avatars/${user.avatarId}.png`} />;
    }
    else return <img src={`/avatars/generic.png`} />;
};
 
const UserCardWithAvatar = ({user}) => (
    <UserCard user={user}>
        <UserAvatar user={user} />
    </UserCard>
);

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:

class LogMessage implements LogMessage {
    public function __construct(
        public readonly string $message) : void;
}
 
class LogMessageWithEmail extends LogMessage implements Mailable {
    public function getEmailContent() : string {
        return "New Log Message: {$this -> message}";
    }
}
 
class LogMessageWithUser extends LogMessage implements RelatedToUser {
 
    public function __construct(
        public readonly string $message,
        public readonly int $userId) {}
 
    public function getTargetUserId() : int {
        return $this -> userId;
    }
 
}

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.

Advertisement

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.

Profile Photo for James Walker James Walker
James Walker is a contributor to How-To Geek DevOps. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes.
Read Full Bio »