I'm currently writing a small console application on the Symfony 2 framework. I'm attempting to insulate the application from the framework (mainly as an exercise after hearing some interesting talks on hexagonal architecture/ports and adaptors, clean code and decoupling applications from frameworks), so that it could potentially be run as a console application, a web application, or moved to another framework with little effort.
The issue I'm having is when one of my interfaces is implemented using the adaptor pattern and it depends on another interface that is also implemented using the adaptor pattern. It's difficult to describe and is probably best described with a code example. Here I've prefixed my class/interface names with "My", just to make it clear which code is my own (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)
{
// ...
}
}
One extra thing to note is that \Symfony\Component\Console\Output\ConsoleOutput
implements \Symfony\Component\Console\Output\OutputInterface
.
To conform to MyDialogInterface
, the MySymfonyDialogAdaptor::askConfirmation
method must take an instance of MyOutputInterface
as an argument. However, the call to Symfony's DialogHelper::askConfirmation
method expects an instance of \Symfony\Component\Console\Output\OutputInterface
, meaning the code won't run.
I can see a couple of ways around this, neither of which are particularly satisfactory:
Have
MySymfonyOutputAdaptor
implement bothMyOutputInterface
andSymfony\Component\Console\Output\OutputInterface
. This isn't ideal, as I'd need to specify all of the methods in that interface, when my application only really cares about thewriteln
method.Have
MySymfonyDialogAdaptor
assume that the object passed to it is an instance ofMySymfonyOutputAdaptor
: If it's not, then throw an exception. Then add a method to theMySymfonyOutputAdaptor
class to obtain the underlying\Symfony\Component\Console\Output\ConsoleOutput
object, which can be passed to Symfony'sDialogHelper::askConfirmation
method directly (as it implements Symfony'sOutputInterface
). This would look something like the following: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 feels wrong: If
MySymfonyDialogAdaptor::askConfirmation
has a requirement that its first argument is an instance of MySymfonyOutputAdaptor, it should specify it as its typehint, but that would mean it no longer implementsMyDialogInterface
. Also, accessing the underlyingConsoleOutput
object outside of its own adaptor doesn't seem ideal, as it should really be wrapped by the adaptor.
Can anyone suggest a way around this? I feel like I'm missing something: Perhaps I'm putting adaptors in the wrong places and rather than multiple adaptors, I just need one adaptor wrapping the whole output/dialog system? Or maybe there's another inheritance layer I need to include in order to implement both interfaces?
Any advice appreciated.
EDIT: This issue is very similar to the one described in the following pull-request: https://github.com/SimpleBus/CommandBus/pull/2