Гексагональная архитектура / чистый код: проблемы с реализацией шаблона адаптера
В настоящее время я пишу небольшое консольное приложение на платформе Symfony 2. Я пытаюсь изолировать приложение от фреймворка (в основном в качестве упражнения после прослушивания некоторых интересных выступлений о гексагональной архитектуре / портах и адаптерах, чистом коде и отделении приложений от фреймворков), чтобы оно могло потенциально выполняться как консольное приложение, веб-приложение, или перешел на другой фреймворк без особых усилий.
У меня проблема, когда один из моих интерфейсов реализован с использованием шаблона адаптера, и это зависит от другого интерфейса, который также реализован с использованием шаблона адаптера. Это сложно описать и, вероятно, лучше всего описать на примере кода. Здесь я поставил перед своими именами классов / интерфейсов "My", чтобы было ясно, какой код принадлежит мне (и я могу редактировать), а какой - в фреймворке Symfony.
// 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)
{
// ...
}
}
Еще одна вещь, на которую следует обратить внимание: \Symfony\Component\Console\Output\ConsoleOutput
инвентарь \Symfony\Component\Console\Output\OutputInterface
,
Соответствовать MyDialogInterface
, MySymfonyDialogAdaptor::askConfirmation
метод должен взять экземпляр MyOutputInterface
в качестве аргумента. Тем не менее, призыв к Symfony's DialogHelper::askConfirmation
метод ожидает экземпляр \Symfony\Component\Console\Output\OutputInterface
это означает, что код не будет работать.
Я вижу несколько способов обойти это, ни один из которых не является особенно удовлетворительным:
Есть
MySymfonyOutputAdaptor
реализовать обаMyOutputInterface
а такжеSymfony\Component\Console\Output\OutputInterface
, Это не идеально, так как мне нужно указать все методы в этом интерфейсе, когда мое приложение действительно заботится только оwriteln
метод.Есть
MySymfonyDialogAdaptor
Предположим, что переданный ему объект является экземпляромMySymfonyOutputAdaptor
Если это не так, тогда выведите исключение. Затем добавьте метод кMySymfonyOutputAdaptor
класс для получения основного\Symfony\Component\Console\Output\ConsoleOutput
объект, который может быть передан Symfony'sDialogHelper::askConfirmation
метод напрямую (так как он реализует Symfony'sOutputInterface
). Это будет выглядеть примерно так: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); } }
Это неправильно: если
MySymfonyDialogAdaptor::askConfirmation
имеет требование, чтобы его первый аргумент был экземпляром MySymfonyOutputAdaptor, он должен указывать его в качестве типовой подсказки, но это означало бы, что он больше не реализуетMyDialogInterface
, Кроме того, доступ к основномуConsoleOutput
объект вне его собственного адаптера не кажется идеальным, поскольку он должен быть действительно обернут адаптером.
Кто-нибудь может предложить способ обойти это? Я чувствую, что что-то упустил: возможно, я помещаю адаптеры не в те места, а вместо нескольких адаптеров, мне просто нужен один адаптер, охватывающий всю систему вывода / диалога? Или, может быть, есть другой уровень наследования, который мне нужно включить для реализации обоих интерфейсов?
Любой совет приветствуется.
РЕДАКТИРОВАТЬ: Эта проблема очень похожа на проблему, описанную в следующем запросе по запросу: https://github.com/SimpleBus/CommandBus/pull/2
1 ответ
После долгих обсуждений с коллегами (спасибо Яну и Оуэну), а также некоторой помощи от Матиаса через https://github.com/SimpleBus/CommandBus/pull/2, мы пришли к следующему решению:
<?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's 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)
{
// ...
}
}
Я думаю, что концепция, которую я упустил, заключалась в том, что адаптеры по существу однонаправлены (например, из моего кода в Symfony или наоборот) и что мне нужен был другой отдельный адаптер для преобразования из MyOutputInterface
назад к Symfony's OutputInterface
учебный класс.
Это не совсем идеально, так как мне все еще нужно реализовать все методы Symfony в этом новом адаптере (MyOutputToSymfonyOutputAdapter
), но эта архитектура выглядит достаточно хорошо структурированной, поскольку ясно, что каждый адаптер конвертируется в одном направлении: я переименовал адаптеры соответствующим образом, чтобы сделать это более понятным.
Другой альтернативой было бы полностью реализовать только те методы, которые я хотел поддержать (просто writeln
в этом примере) и определите другие методы, которые выдают исключение, чтобы указать, что адаптер не поддерживает их, если они вызваны.
Большое спасибо за помощь всем.