Does applying the pipeline design pattern increase testability?

Pretty general question about the pipe or pipeline design pattern and how it relates to purity and testability.

The code is a case of a read-read-process pipe, that is, two reads from database/file that depend on each other, and both stop if they find null.

This is the code, taken from our real code-base:

function getAttributesFromTheme(string $themeName)
{
    $theme = $this->getTheme($themeName);
    if (empty($theme)) {
        return null;
    }
    $xml = $this->getXmlFromTheme($theme);
    if (empty($xml)) {
        return null;
    }
    return $this->extractAttributes($xml);
}

function caller()
{
    $attributes = $this->getAttributesFromTheme('mytheme');
}

function getAttributesFromTheme(string $themeName)
{
    return Pipe::make(
        $this->getTheme(...),
        $this->getXmlFromTheme(...),
        $this->extractAttributes(...)
    )
    ->stopIfEmpty()
    ->from($themeName);
}

function caller()
{
    $attributes = $this->getAttributesFromTheme('mytheme')->run();
}

The interesting thing to notice is that getAttributesFromTheme is a pure function in the second case, meaning it has no side-effects. It only decides how to organize effectful code without applying any of the effects itself.

On the other hand you now give more responsibility to the calling code caller, that has to:

  1. Know it gets a pipeline object
  2. Execute the pipeline with run()

Pure functions are, in general, easier to test. You don’t have to care about state.

The pipeline improves testability since you can separately test:

  1. The pipeline mechanism itself.
  2. Individual steps.
  3. Process a whole.

It is similar to middleware approach used in many cases in Yii3.

2 Likes

True. Middleware as defined by PSR is a little heavier and relies on classes more than methods, so I think piping methods together is an interesting alternative to work with inside classes. Still, I do have some doubts related to readability and implicit flow of the code (in this case, stop at empty or pass result to next function). It’s reminicent of Forth and its implicit stack.

There might be some reasons to extend the use-case of the pipe design pattern. For example adding a $pipe->foreach($items); method to repeat the pipe’s stages on a collection, or combine it with Amphp or Swoole to achieve concurrency.