【问题标题】:Hexagonal architecture/clean code: Problems implementing adaptor pattern六边形架构/干净代码:实现适配器模式的问题
【发布时间】:2014-11-02 12:08:37
【问题描述】:

我目前正在 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的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 对象似乎并不理想,因为它实际上应该由适配器包装。

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

任何建议表示赞赏。

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

【问题讨论】:

  • 你可以尝试让你的实现来实现你自己的接口,也可以扩展使用它们的 symfony 组件。这样你就可以与 Symfony 一起实现,而无需任何额外的工作。

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


【解决方案1】:

经过与同事的大量讨论(感谢 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)并定义其他方法以引发异常以指示它们在调用时不受适配器支持。

非常感谢大家的帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多