Hexagonal architecture / clean code: problems with the adapter pattern implementation

I am currently writing a small console application on the Symfony 2 platform. I am trying to isolate the application from the framework (mainly as an exercise after listening to some interesting talk about hexagonal architecture / ports and adapters, cleaning up code, and decoupling applications from frameworks) so that it could be run as a console application, a web application, or moved to a different framework with a little effort.

The problem I am running into is when one of my interfaces is implemented using the adapter pattern and it depends on another interface that is also implemented using the adapter pattern. This is difficult to describe and is probably best described with sample code. Here I am prefixing the class / interface names with "Mine" to make it clear which code is mine (and I can edit) and which belongs to the Symfony framework.

// My code.

interface MyOutputInterface
{
    public function writeln($message);
}

class MySymfonyOutputAdaptor implements MyOutputInterface
{
    private $output;

    public function __construct(\Symfony\Component\Console\Output\ConsoleOutput $output)
    {
        $this->output = $output;
    }

    public function writeln($message)
    {
        $this->output->writeln($message)
    }
}

interface MyDialogInterface
{
    public function askConfirmation(MyOutputInterface $output, $message);
}

class MySymfonyDialogAdaptor implements MyDialogInterface
{
    private $dialog;

    public function __construct(\Symfony\Component\Console\Helper\DialogHelper $dialog)
    {
        $this->dialog = $dialog;
    }

    public function askConfirmation(MyOutputInterface $output, $message)
    {
        $this->dialog->askConfirmation($output, $message); // Fails: Expects $output to be instance of \Symfony\Component\Console\Output\OutputInterface
    }
}

// Symfony code.

namespace Symfony\Component\Console\Helper;

class DialogHelper
{
    public function askConfirmation(\Symfony\Component\Console\Output\OutputInterface $output, $question, $default = true)
    {
        // ...
    }
}

      

It should be noted that it \Symfony\Component\Console\Output\ConsoleOutput

implements \Symfony\Component\Console\Output\OutputInterface

.

To match MyDialogInterface

, a method MySymfonyDialogAdaptor::askConfirmation

must take an instance MyOutputInterface

as an argument. However, the symfony method call DialogHelper::askConfirmation

expects an instance \Symfony\Component\Console\Output\OutputInterface

, which means the code won't work.

I see several ways to work around this, none of which are particularly satisfying:

  • Have to MySymfonyOutputAdaptor

    implement both MyOutputInterface

    and Symfony\Component\Console\Output\OutputInterface

    . This is not ideal as I will need to specify all methods in this interface when my application really cares a lot about the method writeln

    .

  • Suppose the MySymfonyDialogAdaptor

    object passed to it is an instance MySymfonyOutputAdaptor

    : if it isn't, throw an exception. Then add a method to the class MySymfonyOutputAdaptor

    to get a base object \Symfony\Component\Console\Output\ConsoleOutput

    that can be passed directly to the Symfony method DialogHelper::askConfirmation

    (since it implements Symfony OutputInterface

    ). It looks something like this:

    class MySymfonyOutputAdaptor implements MyOutputInterface
    {
        private $output;
    
        public function __construct(\Symfony\Component\Console\Output\ConsoleOutput $output)
        {
            $this->output = $output;
        }
    
        public function writeln($message)
        {
            $this->output->writeln($message)
        }
    
        public function getSymfonyConsoleOutput()
        {
            return $this->output;
        }
    }
    
    class MySymfonyDialogAdaptor implements MyDialogInterface
    {
        private $dialog;
    
        public function __construct(\Symfony\Component\Console\Helper\DialogHelper $dialog)
        {
            $this->dialog = $dialog;
        }
    
        public function askConfirmation(MyOutputInterface $output, $message)
        {
            if (!$output instanceof MySymfonyOutputAdaptor) {
                throw new InvalidArgumentException();
            }
    
            $symfonyConsoleOutput = $output->getSymfonyConsoleOutput();
    
            $this->dialog->askConfirmation($symfonyConsoleOutput, $message);
        }
    }
    
          

    This seems to be wrong: if it MySymfonyDialogAdaptor::askConfirmation

    has a requirement that its first argument be an instance of MySymfonyOutputAdaptor, it must specify it as its type, but that would mean that it no longer implements MyDialogInterface

    . Also, accessing the underlying object ConsoleOutput

    outside of its own adapter doesn't seem ideal, as it really needs to be wrapped by an adapter.

Can anyone suggest a way around this? I feel like I'm missing something: perhaps I am putting adapters in the wrong places and instead of multiple adapters, I only need one adapter that wraps the entire output / dialog system? Or maybe there is another level of inheritance that I need to enable to implement both interfaces?

Any advice is appreciated.

EDIT: This issue is very similar to the one described in the following pull-request: https://github.com/SimpleBus/CommandBus/pull/2

+3


source to share


1 answer


After much discussion with colleagues (thanks to Ian and Owen) and also some help from Matthias via https://github.com/SimpleBus/CommandBus/pull/2 , we came up with the following solution:

<?php

// My code.

interface MyOutputInterface
{
    public function writeln($message);
}

class SymfonyOutputToMyOutputAdaptor implements MyOutputInterface
{
    private $output;

    public function __construct(\Symfony\Component\Console\Output\OutputInterface $output)
    {
        $this->output = $output;
    }

    public function writeln($message)
    {
        $this->output->writeln($message)
    }
}

class MyOutputToSymfonyOutputAdapter implements Symfony\Component\Console\Output\OutputInterface
{
    private $myOutput;

    public function __construct(MyOutputInterface $myOutput)
    {
        $this->myOutput = $myOutput;
    }

    public function writeln($message)
    {
        $this->myOutput->writeln($message);
    }

    // Implement all methods defined in Symfony OutputInterface.
}

interface MyDialogInterface
{
    public function askConfirmation(MyOutputInterface $output, $message);
}

class MySymfonyDialogAdaptor implements MyDialogInterface
{
    private $dialog;

    public function __construct(\Symfony\Component\Console\Helper\DialogHelper $dialog)
    {
        $this->dialog = $dialog;
    }

    public function askConfirmation(MyOutputInterface $output, $message)
    {
        $symfonyOutput = new MyOutputToSymfonyOutputAdapter($output);

        $this->dialog->askConfirmation($symfonyOutput, $message);
    }
}

// Symfony code.

namespace Symfony\Component\Console\Helper;

class DialogHelper
{
    public function askConfirmation(\Symfony\Component\Console\Output\OutputInterface $output, $question, $default = true)
    {
        // ...
    }
}

      

I think the concept I was missing was that adapters are essentially unidirectional (from my code to Symfony, for example, or vice versa) and that I need another separate adapter to convert from MyOutputInterface

to a Symfony class OutputInterface

.



This is not entirely ideal as I still need to implement all the Symfony methods in this new adapter ( MyOutputToSymfonyOutputAdapter

), but this architecture is really well structured as it is clear that each adapter converts to one direction: I renamed the adapters to make it clearer.

Another alternative would be to fully implement only the methods that I wanted to support (just writeln

in this example) and define other methods to throw exceptions to indicate that they are not supported by the adapter if called.

Many thanks for the help.

+2


source







All Articles