设计对象渲染系统

时间:2016-01-15 22:41:35

标签: php oop design-patterns interface

我编写了一些代码,其中包含一组实现简单接口的对象。这些对象只是普通的DTO。他们需要渲染。每个人都需要它自己的渲染器。我最初认为好,所以会有一个渲染器接口,它有一个方法render,它会接受一个结果ResultInterface。每个结果项都有不同的额外数据需要呈现。

所以实际发生的是每个渲染器然后检查它是否接收到正确的类型。因此,尽管它似乎接受了实施ResultInterface的任何内容,但它实际上并没有。然后我想,为什么我甚至打扰ResultInterface上的类型提示。

以下是一些示例:

<?php

Interface RendererInterface
{
    public function render(ResultInterface $result);
}


class ExceptionFailureRenderer implements RendererInterface
{
    public function render(ResultInterface $result)
    {
        if (!$result instanceof ExceptionFailure) {

            throw new InvalidArgumentException;
        }
    }
}

class SomeOtherFailureRenderer implements RendererInterface
{
    public function render(ResultInterface $result)
    {
        if (!$result instanceof SomeOtherFailure) {

            throw new InvalidArgumentException;
        }
    }
}

Interface ResultInterface
{
    public function getName();
}

class ExceptionFailure implements ResultInterface
{
    public function getName()
    {
        return 'Exception Failure';
    }

    public function getException()
    {
        return $this->exception;
    }
}

class SomeOtherFailure implements ResultInterface
{
    public function getName()
    {
        return 'Some Other Failure';
    }

    public function getSomeOtherPieceOfData()
    {
        return $this->importantData;
    }
}

$renderers = [
    ExceptionFailure::class => new ExceptionFailureRenderer,
    SomeOtherFailure::class => new SomeOtherFailureRenderer
];

$output = '';
foreach ($results as $result) {
    $renderer = $renderers[get_class($result)];
    $output  .= $renderers->render($result);
}

问题

Renderer只期望ResultInterface时,如何更好地避免调用具体方法?

2 个答案:

答案 0 :(得分:1)

您正在使用具有已定义函数的接口,这些接口实现它们的类实际上并未使用。

class ExceptionFailureRenderer (missing implements Renderer)
{
    public function render(ResultInterface $result)
    {
        if (!$result instanceof ExceptionFailure) {

            throw new InvalidArgumentException;
        }
    }
}

如果我要编写这段代码,我会使用一个抽象类,用I前缀加入I到IRenderer和IResultInterface(避免类之间的混淆);

abstract class Renderer implements IRenderer
{ 
        public function render(IResultInterface $result)
        {
            //abstract logic here
        }    
}

class ExceptionFailureRenderer extends Renderer
{
    //overide the logic if required
}

此外,我将使用RendererFactory,存储库或类似的设计模式来管理要使用的渲染。这样可以防止某些与某些Renderer对象的耦合。

class RenderFactory {

     protected $instances = [];

     public function registerRenderer(IRenderer $renderer) {
          $this->instances[$renderer::class] = $renderer;
     }  

     public function doRender(ResultInterface $resultInterface) {
         //logic to retrive the renderer
     }

}

$renderFactory = new RenderFactory();
$renderFactory->registerRenderer(new ExceptionFailureRenderer());
$renderFactory->doRender($exceptionFaliureResult);

这当然是优惠的,这不仅仅是对如何做到这一点的回答。我认为你的答案很模糊,因为没有答案。

答案 1 :(得分:1)

你说这里存在设计缺陷是对的。 拥有一个实现接口的对象只会在履行隐含合同时抛出异常,这是一个明显的代码味道。

有很多方法可以重构这个,我就是这样做的。

查看具体的RendererInterface实现,显然它们与具体的ResultInterface相关联,因此应该明确声明该依赖关系。 (WTF ??见底部注释)

由于不可变对象通常更容易使用,并且似乎没有合理的理由来重用RendererInterface实现(实例化它似乎并不昂贵,或者这样做有任何副作用)我决定通过使RendererInterface方法无参数来改变render,并将具体的结果对象注入构造函数中:

Interface RendererInterface
{
    public function render();
}


class ExceptionFailureRenderer implements RendererInterface
{
    private $result;
    public function __construct(ExceptionFailure $result)
    {
        $this->result = $result;
    }
    public function render()
    {
        echo '<p>'.$this->result->getException().'</p>';
    }
}

class SomeOtherFailureRenderer implements RendererInterface
{
    private $result;
    public function __construct(SomeOtherFailure $result)
    {
        $this->result = $result;
    }
    public function render()
    {
        echo '<p>'.$this->result->getSomeOtherPieceOfData().'</p>';
    }
}

现在混凝土渲染器与它们所需的具体结果相关联,您需要一种方法来获得具体结果的正确渲染器。 显而易见的答案是工厂:

interface RenderFactoryInterface
{
    /***
     * @param ResultInterface $result
     * @return RendererInterface
     */
    public function getRenderer(ResultInterface $result);
}

class RenderFactory implements RenderFactoryInterface
{

    //could be injected into constructor
    private $renderMap = [
        ExceptionFailure::class => ExceptionFailureRenderer::class,
        SomeOtherFailure::class => SomeOtherFailureRenderer::class

    ];


    public function getRenderer(ResultInterface $result)
    {
        if(!isset($this->renderMap[get_class($result)])){
            //write a more suitable exception class
            throw new InvalidArgumentException();
            //or perhaps return a defaultRenderer that only works on
            //the methods defined in `ResultInterface`
        }
        return new $this->renderMap[get_class($result)]($result);
    }
}

您的消费代码只需要ResultInterface个对象的集合和RenderFactoryInterface的实现来完成其工作,不知道集合中特定类型的ResultInterface,也不知道任何关于如何将ResultInterfaceRendererInterface

联系起来的知识
class Consumer
{
    private $results;
    private $renderFactory;

    /***
     * @param ResultInterface[] $results php doesnt have typed collections let
     * @param RenderFactoryInterface $renderFactory
     */
    public function __construct(array $results, RenderFactoryInterface $renderFactory)
    {
        $this->results = $results;
        $this->renderFactory = $renderFactory;
    }

    public function doSomething()
    {
        foreach($this->results as $result){
            $this->renderFactory->getRenderer($result)->render();
        }
    }
}

注意上面是使用php 5,它没有声明返回类型的方法,php 7会让你更具体地说getRenderer方法应该返回RendererInterface的实现。 此外,消费者代码只能输入提示数组,希望类型化的集合很快就会到达。

此外,作为"Typehint a concrete class?? WTF are you thinking"的先发制人,本例旨在尽可能简短。你当然可以创建一个ExceptionFailureInterface和一个SomeOtherFailureInterface,输入那些,并让具体的ResultInterface类实现正确的(和ResultInterface)如果你认为增加灵活性值得增加复杂性。 我对这个答案的观点是,具体的RendererInterface类需要一个比ResultInterface更具体的契约,可以是一个具体的类(如图所示)或更具体的接口