Initial Situation

Conditional code (switch. if-else, etc.) is used to determine execution flow.

Goal

Create a class (a Command) for each execution path and add code for fetching and executing them.

Improves

Long Method   

Difficulty

medium

Pros/Cons

Simple way of executing diverse behavior in a uniform way.
Allows runtime modification of available commands.
Is easy to implement.
Makes code more complex if no additional flexibility is needed.
Setup Continuous Code Quality Management in minutes.

Refactoring Basics

Replace Conditional Dispatcher with Command

Overview

A conditional dispatcher is a conditional statement (typically switch) which is used to change the execution flow of your code.

Conditional dispatchers are not necessarily bad, some are well suited for the task they are performing. This applies mostly to dispatchers which are small, but even small Conditional Dispatchers might be better refactored to the Command pattern.

If one of the following is the case for you, you can benefit from using a Command pattern over a Conditional dispatcher:

  • Need for Runtime Flexibility: You want to pass in requests which you do not know of (for example, because they are provided by third-party libraries).
  • Long Conditional Dispatchers with big chunks of code

The Command pattern simply places the action related code into a separate class, the Command class. A command class typically has a execute or run method.

If you are not sure yet whether you should keep a Conditional Dispatcher or use a Command pattern, keep the Conditional Dispatcher. The Command pattern is very easy to refactor to if the need arises.

Note: Commands are not necessarily related to the CLI, this pattern is frequently applied in other scenarios as we can see in the example.

Further Resources

Checklist

Step 1
Locate the Request Handling Code
On the class which contains the conditional dispatcher, find the request handling code and extract it to a method.
Step 2
Repeat step 1
Repeat step 1 for all request-handling code chunks. So that each handling code has its own method.
Step 3
Extract request-handling methods
Next, extract each request-handling method to a new class, the Command class. The request-handling method is then typically named ``execute`` or ``run``.
Step 4
Define a Command base class/interface
Once you have extracted all methods to Command classes, analyze them to see which code they have in common. This typically means: Determining common dependencies, determening common parameters, determining the simplest signature.
Step 5
Extend/Implement the base class/interface
Step 6
Create a map containing all commands in the class containing the Conditional Dispatcher
Step 7
Replace the Conditional Dispatcher with code retrieving the correct command and executing it

Example: Background Worker

Background

As example, we will refactor a Worker class which subscribes to several AMQP queues and processes incoming messages. This class was taken from a real-world open-source project and was slightly adapted for the purposes of this example:

class WorkflowWorker
{
    private $entityManager;
    private $serializer;

    public function __construct(EntityManager $entityManager, Serializer $serializer, Channel $channel)
    {
        $this->entityManager = $entityManager;
        $this->serializer = $serializer;

        $channel->subscribe(array($this, 'onMessage'));
    }

    public function onMessage($queue, AMQPMessage $message)
    {
        switch ($queue) {
            case 'workflow_execution_termination':
                return $this->consumeExecutionTermination($message);

            case 'workflow_execution_details':
                return $this->consumeExecutionDetails($message);

            default:
                throw new \LogicException('Unsupported queue.');
        }
    }

    public function consumeExecutionDetails(AMQPMessage $message)
    {
        $input = json_decode($message->body, true);

        if ( ! isset($input['execution_id'])) {
            throw new \InvalidArgumentException('"execution_id" attribute was not set.');
        }

        $execution = $this->entityManager->find('Workflow:WorkflowExecution', $input['execution_id']);
        if (null === $execution) {
            throw new \InvalidArgumentException(
                sprintf('There is no execution with id "%s".', $input['execution_id']));
        }

        return $this->serializer->serialize($execution, 'json');
    }

    public function consumeExecutionTermination(AMQPMessage $message)
    {
        $input = json_decode($message->body, true);

        /** @var $execution WorkflowExecution */
        $execution = $this->entityManager->getRepository('Workflow:WorkflowExecution')
                                            ->getByIdExclusive($input['execution_id']);

        $execution->terminate();
        $this->entityManager->persist($execution);
        $this->entityManager->flush();

        return '';
    }

    // ...
}

Step 1: Locate the Request Handling Code

This step is quite easy as the handling code was already extracted into separate methods. Basically, both callback methods (consumeExecutionDetails and consumeExecutionTermination) qualify as request-handling code and will be moved to command classes.

Step 2: Repeat Step 1

We can safely skip this as all methods are already extracted.

Step 3: Extract Request-Handling Methods

Finally, it’s time to write some code, or more precisely move code around. We will extract all the request-handling methods to their own classes. We will not yet change any code though.

Tip: If commands call other methods on the class containing the conditional dispatcher, i.e. WorkflowWorker, we can temporarily make these methods public and inject the worker as a dependency into the respective commands.
class ConsumeExecutionDetailsCommand
{
    private $entityManager;
    private $serializer;

    public function __construct(EntityManager $entityManager, Serializer $serializer)
    {
        $this->entityManager = $entityManager;
        $this->serializer = $serializer;
    }

    public function execute(AMQPMessage $message)
    {
        $input = json_decode($message->body, true);

        if ( ! isset($input['execution_id'])) {
            throw new \InvalidArgumentException('"execution_id" attribute was not set.');
        }

        $execution = $this->entityManager->find('Workflow:WorkflowExecution', $input['execution_id']);
        if (null === $execution) {
            throw new \InvalidArgumentException(
                sprintf('There is no execution with id "%s".', $input['execution_id']));
        }

        return $this->serializer->serialize($execution, 'json');
    }
}

class ConsumeExecutionTerminationCommand
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function execute(AMQPMessage $message)
    {
        $input = json_decode($message->body, true);

        /** @var $execution WorkflowExecution */
        $execution = $this->entityManager->getRepository('Workflow:WorkflowExecution')
                                            ->getByIdExclusive($input['execution_id']);

        $execution->terminate();
        $this->entityManager->persist($execution);
        $this->entityManager->flush();

        return '';
    }
}

class WorkflowWorker
{
    private $executionDetailsCommand;
    private $executionTerminationCommand;

    public function __construct(EntityManager $entityManager, Serializer $serializer, Channel $channel)
    {
        $this->executionDetailsCommand = new ConsumeExecutionDetailsCommand($entityManager, $serializer);
        $this->executionTerminationCommand = new ConsumeExecutionTerminationCommand($entityManager);

        $channel->subscribe(array($this, 'onMessage'));
    }

    public function onMessage($queue, AMQPMessage $message)
    {
        switch ($queue) {
            case 'workflow_execution_termination':
                return $this->consumeExecutionTermination($message);

            case 'workflow_execution_details':
                return $this->consumeExecutionDetails($message);

            default:
                throw new \LogicException('Unsupported queue.');
        }
    }

    public function consumeExecutionDetails(AMQPMessage $message)
    {
        return $this->executionDetailsCommand->execute($message);
    }

    public function consumeExecutionTermination(AMQPMessage $message)
    {
        return $this->executionTerminationCommand->execute($message);
    }

    // ...
}

The request-handling logic is now in their own classes. The code is not really desirable yet as we have some duplication, but the important part is that to all outside classes nothing has changed. You can also still run your tests and they should still pass. In practice, you can of course skip steps and arrange the code in its final form directly without performing a step-by-step transformation.

Step 4 & 5: Define a Command base class/interface and extend/implement it

This step requires some analysis of the extracted commands. If we take a look at both, we see that both commands have a dependency on the EntityManager this is a first candidate for placing it in a base command. While the Serializer is only a dependency for one command, we will still pull it up into the base class as it seems general enough to be useful for future commands.

The choice for the signature of the execute method is quite easy as it is already the same for both classes. There is no need to change it.

Our final command classes looks like this:

abstract class Command
{
    private $entityManager;
    private $serializer;

    public function __construct(EntityManager $entityManager, Serializer $serializer)
    {
        $this->entityManager = $entityManager;
        $this->serializer = $serializer;
    }

    abstract public function execute(AMQPMessage $message);
}

class ConsumeExecutionDetailsCommand extends Command
{
    public function execute(AMQPMessage $message)
    {
        // ...
    }
}

class ConsumeExecutionTerminationCommand extends Command
{
    public function execute(AMQPMessage $message)
    {
        // ...
    }
}

Step 6 & 7: Define Command Map and Replace Conditional Dispatcher

Now, the last thing left to do is to clean up the WorkflowWorker. We will remove the fields for the different commands and replace them with an array. Besides we will also get rid of the switch statement for dispatching the request:

class WorkflowWorker
{
    private $commands = array();

    public function __construct(EntityManager $entityManager, Serializer $serializer, Channel $channel)
    {
        $this->commands = array(
            'workflow_execution_termination' =>
                new ConsumeExecutionTerminationCommand($entityManager, $serializer),
            'workflow_execution_details' =>
                new ConsumeExecutionDetailsCommand($entityManager, $serializer),
        );

        $channel->subscribe(array($this, 'onMessage'));
    }

    public function onMessage($queue, AMQPMessage $message)
    {
        if ( ! isset($this->commands[$queue])) {
            throw new \LogicException('Unsupported queue.');
        }

        return $this->commands[$queue]->execute($message);
    }

    // ...
}

That’s it! We successfully refactored the WorkflowWorker to the command pattern.

How to use the Checklist?

Checklists present a proven order of necessary refactoring steps and are especially helpful for beginners.

If you feel comfortable with a refactoring, there is no need to follow each of the steps literally; you can often perform multiple steps at once or skip some.

We suggest to try the recommended order at least once; this will often provide new insights - even for seasoned developers.