В настоящее время я пишу небольшое консольное приложение на платформе 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’s DialogHelper::askConfirmation
метод напрямую (так как он реализует Symfony’s OutputInterface
). Это будет выглядеть примерно так:
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
объект вне его собственного адаптера не кажется идеальным, так как он действительно должен быть обернут адаптером.
Кто-нибудь может предложить способ обойти это? Я чувствую, что что-то упустил: возможно, я помещаю адаптеры не в те места, а вместо нескольких адаптеров, мне просто нужен один адаптер, охватывающий всю систему вывода / диалога? Или, может быть, есть другой уровень наследования, который мне нужно включить для реализации обоих интерфейсов?
Любой совет приветствуется.
РЕДАКТИРОВАТЬ: Эта проблема очень похожа на проблему, описанную в следующем pull-запросе: https://github.com/SimpleBus/CommandBus/pull/2
После долгих обсуждений с коллегами (спасибо Иэну и Оуэну), а также помощи от Матиаса через 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
в этом примере) и определите другие методы, которые выдают исключение, чтобы указать, что адаптер не поддерживает их, если они вызваны.
Большое спасибо за помощь всем.
Других решений пока нет …