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 bothMyOutputInterface
andSymfony\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 methodwriteln
. -
Suppose the
MySymfonyDialogAdaptor
object passed to it is an instanceMySymfonyOutputAdaptor
: if it isn't, throw an exception. Then add a method to the classMySymfonyOutputAdaptor
to get a base object\Symfony\Component\Console\Output\ConsoleOutput
that can be passed directly to the Symfony methodDialogHelper::askConfirmation
(since it implements SymfonyOutputInterface
). 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 implementsMyDialogInterface
. Also, accessing the underlying objectConsoleOutput
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
source to share
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.
source to share