六角形架构/清洁代码:实现适配器模式的问题

时间:2014-11-02 12:08:37

标签: php oop symfony design-patterns hexagonal-architecture

我目前正在Symfony 2框架上编写一个小型控制台应用程序。我尝试将应用程序与框架隔离开来(主要是在听到关于六边形体系结构/端口和适配器,干净代码以及将应用程序与框架分离的一些有趣的讨论之后的练习),以便它可以作为控制台运行应用程序,Web应用程序,或者只需很少的努力就可以移动到另一个框架。

我遇到的问题是当我的某个接口使用适配器模式实现时,它依赖于另一个也使用适配器模式实现的接口。它很难描述,最好用代码示例来描述。在这里,我使用" 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

要符合MyDialogInterfaceMySymfonyDialogAdaptor::askConfirmation方法必须将MyOutputInterface的实例作为参数。但是,对Symfony的DialogHelper::askConfirmation方法的调用需要\Symfony\Component\Console\Output\OutputInterface的实例,这意味着代码不会运行。

我可以看到几种解决方法,但这两种方式都不是特别令人满意:

  1. MySymfonyOutputAdaptor同时实施MyOutputInterfaceSymfony\Component\Console\Output\OutputInterface。这并不理想,因为当我的应用程序真正关心writeln方法时,我需要指定该界面中的所有方法。

  2. MySymfonyDialogAdaptor假设传递给它的对象是MySymfonyOutputAdaptor的实例:如果不是,则抛出异常。然后向MySymfonyOutputAdaptor类添加一个方法以获取基础\Symfony\Component\Console\Output\ConsoleOutput对象,该对象可以直接传递给Symfony的DialogHelper::askConfirmation方法(因为它实现了Symfony' s { {1}})。这看起来如下所示:

    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); } } 要求其第一个参数是MySymfonyOutputAdaptor的实例,则应将其指定为其typehint,但这意味着它不再实现MySymfonyDialogAdaptor::askConfirmation。此外,访问其自身适配器之外的基础MyDialogInterface对象似乎并不理想,因为它实际上应该由适配器包装。

  3. 有人可以建议解决这个问题吗?我觉得我错过了一些东西:也许我把适配器放在错误的位置而不是多个适配器,我只需要一个适配器包装整个输出/对话系统?或者可能还有我需要包含的另一个继承层才能实现这两个接口?

    任何建议表示赞赏。

    编辑:此问题与以下提取请求中描述的问题非常类似:https://github.com/SimpleBus/CommandBus/pull/2

1 个答案:

答案 0 :(得分:2)

经过与同事的多次讨论(感谢Ian和Owen),再加上Matthias通过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的OutputInterface课程。

这不是完全理想的,因为我仍然必须在这个新的适配器(MyOutputToSymfonyOutputAdapter)中实现所有Symfony的方法,但是这个架构确实感觉结构很好,因为它很明显,每个适配器都在一个方向上进行转换:我已相应地重命名了适配器,以使其更加清晰。

另一种替代方法是完全实现我想要支持的方法(在本例中只是writeln)并定义其他方法抛出异常,表明如果调用它们,它们就不会被适配器支持

非常感谢所有人的帮助。