Гексагональная архитектура / чистый код: проблемы с реализацией шаблона адаптера

В настоящее время я пишу небольшое консольное приложение на платформе 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это означает, что код не будет работать.

Я вижу несколько способов обойти это, ни один из которых не является особенно удовлетворительным:

  1. Есть MySymfonyOutputAdaptor реализовать оба MyOutputInterface а также Symfony\Component\Console\Output\OutputInterface, Это не идеально, так как мне нужно указать все методы в этом интерфейсе, когда мое приложение действительно заботится только о writeln метод.

  2. Есть 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

3

Решение

После долгих обсуждений с коллегами (спасибо Иэну и Оуэну), а также помощи от Матиаса через 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 в этом примере) и определите другие методы, которые выдают исключение, чтобы указать, что адаптер не поддерживает их, если они вызваны.

Большое спасибо за помощь всем.

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]