2016-01-15 6 views
1

Я написал код, который состоит из набора объектов, которые реализуют простой интерфейс. Эти объекты являются просто 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?

ответ

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>'; 
    } 
} 

Теперь, когда конкретные рендеры привязаны к конкретным результатам они require, вам нужен способ получить правильный рендерер при конкретном результате. Очевидный ответ является завод:

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 внутри коллекции, ни любое знание о том, как связать ResultInterface к RendererInterface:

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, может быть, что конкретный класс (как показано) или более конкретный интерфейс

+0

Спасибо, это имеет больше смысла. Немногие: не делает ли это '' __construct' частью контракта 'RendererInterface' сейчас? Что делать, если у рендереров больше и разных зависимостей? Наконец, рендеринги теперь содержат, казалось бы, ненужное состояние. Я согласен с тем, что конкретные интерфейсы для каждого результата кажутся излишними, так как у меня много разных результатов в системе. –

+0

hello @AydinHassan Чтобы ответить на ваши вопросы в обратном порядке, нет ничего выше явно добавленного состояния. Возьмем, например, «ExceptionFailureRenderer» - любой экземпляр должен содержать один экземпляр «ExceptionFailure», это не может быть изменено, другого состояния нет - объект неизменен. Конечно, вы можете изменить это, добавив методы, которые его изменяют (например, сеттер), но мой код не содержит никаких действий. – Steve

+0

Что касается 'RendererInterface', конструктор не является частью контракта. Интерфейс просто гарантирует, что любой объект, который его реализует, содержит открытый, без параметров метод с именем «render». Любой объект, зависящий от экземпляра 'RendererInterface', просто ожидает, что ему будет дан такой объект, он не знает и не заботится о том, как он построен. Конечно, любая конкретная реализация 'RenderFactoryInterface' ** будет ** знать, как построить различные типы, но это ожидалось. – Steve

1

Вы используете интерфейсы с определенными функциями, которые фактически не используются классом, который их реализует.

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

      throw new InvalidArgumentException; 
     } 
    } 
} 

Если бы я должен был написать этот код я хотел бы использовать абстрактный класс Приставка интерфейсы с 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); 

Это, конечно, преференциально, и это не ответ, а просто мнение о том, как я это сделаю. Я думаю, что ваш ответ довольно расплывчатый, поскольку ответа нет.

+0

Я уже делаю все, что вы упоминаете, я просто пропустил это для ясности. Вопрос был больше связан с использованием неопределенных методов. Я знаю, что это vauge, я думаю, что реальный вопрос заключается в использовании неопределенных методов, я буду обновлять сообщение. Спасибо, в любом случае! –

+0

Извинения, если я неправильно понял. Спасибо –

Смежные вопросы