Command Pattern for Legacy Code Refactoring

Just recently I run into an issue on one of my projects which I gracefully solved using the Command pattern. The project has a lot of legacy code, and to avoid the pain of major refactoring we do it gradually, piece by piece.

The existing functionality is pretty straightforward. There’s a report generated, and it gets send over email, or saved into a file in a specified format. An administrator gets to choose what to do with the report.

The problem is that it only allows to do one thing: you either save the report, or send it over email, but you can’t do both. Eventually somebody got tired of emailing the report manually after saving it, so my job was to allow choosing multiple options, e.g. save to a file and email it to somebody.

The simplified UI

The code (simplified) looks like this:

class ReportHandler
{
    public function handle($todo, $report, array $params)
    {
        switch ($todo) {
            case 'csv':
                $csv = ExportCsv();
                return $csv->save($report, $params['filename']);
                break;
            case 'xlsx':
                $xlsx = ExportXlsx();
                return $xlsx->save($report, $params['filename']);
                break;
            case 'email':
                $email = EmailSender();
                return $email->sendWithAttachment($params['addressee'], $report);
                break;
        }
        
        return false;
    }
}

At first it seems like a quick fix, and you can definitely adjust the functionality by adding a few lines of code. However, I decided to use that opportunity to get rid of some technical debt.

Anyone who’s done his time refactoring can testify that encapsulation is often breached, and even if you believe your little change in an existing component won’t affect anything but the functionality you’re working on, you’re probably wrong. And here comes the Command pattern, allowing us to advance the functionality without modifying the existing classes.

We will wrap our functionality classes (ExportCsv, ExportXlsx, EmailSender) into commands of a single interface, so neither of them is going to be modified.

First, let’s create our interface. We only need one method, so it’s going to be a pretty simple one:

interface ReportActionInterface
{
    public function handle($report, array $params);
}

Now we need to create the commands from the diagram above. Each command wraps a single class, and implements the ReportCommandInterface.

class ExportCsvCommand implements ReportCommandInterface
{
    public function handle($report, array $params)
    {
        $export = new ExportCsv();
        return (bool)$export->save($report, $params['filename']);
    }
}
class ExportXlsxCommand implements ReportCommandInterface
{
    public function handle($report, array $params)
    {
        $export = new ExportXlsx();
        return (bool)$export->save($report, $params['filename']);
    }
}
class EmailSenderCommand implements ReportCommandInterface
{
    public function handle($report, array $params)
    {
        if (empty($params['email'])) {
            throw new Exception("Email address is missing");
        }

        $sender = new EmailSender();
        return (bool)$sender->sendWithAttachment($params['addressee'], $report);
    }
}

Now we adjust our ReportHandler class to use the new command interface:

class ReportHandler
{
    public function handle($todo, $report, array $params)
    {
        $todo = (array)$todo;

        foreach ($todo as $commandName) {
        	$commandName .= 'Command';
        	if (!class_exists($commandName)) {
                        // that's a trick and we actually should use dependency injection instead, but that's beyond the topic of the article
        		continue;
	        }

	        $command = new $commandName();
        	if (!$command instanceof ReportCommandInterface) {
        		continue;
	        }

        	$command->handle($report, $params);
        }

        return true;
    }
}

We would also need to adjust UI to support the extended functionality, but it doesn’t have anything to do with the Command pattern.

As most patterns, the Command pattern can be applied in myriad different situations, and this is just a single example, hopefully it made your understanding of the pattern a little bit more clear.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.