Published on

Settings for Composite Modules

Authors
  • avatar
    Name
    Jaap van der Vis
    Twitter

Introduction

Several months ago I made this blog, but I have not posted anything. I have been quite busy with my MSc thesis, and then I moved to a different place. That is all finished now, so it is time I added a new post. In my work, I came across (what I found to be) an interesting problem, so I am using this post to describe the problem and my solution.

The Situation

For various applications, we have modules that describe business rules. These modules are essentially complicated functions with an input and an output. Each module could also use a SettingBuilder to acquire the settings (properties of the module itself, which can be anything) through a build function, based on the module's input. The business rules required these settings (usually an associative array) to function.

When we created this structure, we usually had one giant module per application because that made the most sense at the time. As the number of shared business rules grew, we decided to restructure the modules to make them from composite components. Composition has several advantages, e.g. better code reuse and less responsibility per component. However, it also introduces some challenges.

Our main challenge was the settings. In the old setup, modules were stand-alone components using associative arrays for the settings. This means that missing or wrongly typed settings were only noticeable at runtime. For the new setup, we wanted typed settings, while not adding unnecessary constraints when combining components.

The Problem - Simplified

We first simplify the problem by ignoring the factory (SettingsBuilder) and instead only look at the direct insertion of settings. This already points us in the right direction for the solution.

Let us say we have the following definition for a module:

class ModuleA extends Module
{
    public function __construct(ModuleASettings $settings)
    {
        parent::__construct([
            new ComponentA($settings),
            new ComponentB($settings),
            new ComponentC($settings),
            ...
        ]);
    }
}

Here Module is a simple wrapper class that handles combining and executing the different components it receives in the constructor, making it irrelevant for this problem. The composite components ComponentA, ComponentB, and ComponentC each need some of the settings, e.g.

  • ComponentA needs settings a and b.
  • ComponentB needs settings d and e.
  • ComponentC needs settings b, c, and e.

We can type these settings and ensure their existence by making interfaces for them and using these in the constructor, here shown only for ComponentA:

interface ComponentASettings
{
    public function getA(): float;
    public function getB(): int;
}

...

class ComponentA
{
    public function __construct(ComponentASettings $settings)
    {
        ...
    }
    ...
}

We can then define `ModuleASettings as:

class ModuleASettings implements ComponentASettings, ComponentBSettings, ComponentCSettings
{
    ...
}

This nicely ensures that ModuleA has the necessary settings for ComponentA, ComponentB, and ComponentC. If it does not, your IDE and static code analyzer (PhpStorm and PHPStan in my case) will complain, and you know immediately where the problem lies. Another nice thing about this structure is that the modules are decoupled: they do not need to know what settings the other modules need and modules can be arbitrarily combined. Sounds good, right?

But what about the SettingsBuilder?

Yeah, that is an issue that we ignored in the simplified problem. In the old setup, each module has its own implementation of this, so we could restrict which builder was used and therefore which settings were available. With the new setup, we instead want to ensure that the return type of the build method implements our settings interface.

This is a use case for Generics, a feature unfortunately not supported in PHP. Fortunately, PhpStorm and PHPStan have a basic implementation through PHPDocs, so we can have type safety during development and in the CI.

To accomplish this, we define the interface for a SettingsBuilder and an implementation as shown below. To understand why we use @template-covariant instead of @template, see this blog post.

/**
 * @template-covariant T
 */
interface SettingsBuilder
{
    /**
      * @return T
      */
    public function build();
}
...
/**
 * @implements SettingsBuilder<ModuleASettings>
 */
class ModuleASettingsBuilder implements SettingsBuilder
{
    /**
      * @return ModuleASettings
      */
    public function build(): ModuleASettings
    {
        ...
    }
}

We can now redefine the components as follows:

class ComponentA
{
    /**
     * @param SettingsBuilder<ComponentASettings> $settings_builder
     */
    public function __construct(SettingsBuilder $settings_builder)
    {
        ...
    }
    ...
}

Though PHP itself has no notion of generics, PhpStorm and PHPStan actually understand that the output of $settings_builder->build() returns an instance of ComponentASettings. Furthermore, PHPStan will complain if your implementation of SettingsBuilder does not provide settings that implement the interface expected by the component.

Neat, right?

Summary

I am quite pleased with this solution. It allows us to ensure the following:

  • Type safety of the settings
  • Existence of the settings necessary for each component.
  • Combine arbitrary components without introducing coupling

We will soon release this to production, so I am curious to see how it will do.