Question

This is a theoretical problem that bothers me. Let's say we have following, simplified structure:

interface ParagraphInterface {}

interface ParagraphGroupInterface
{
    public function getParagraphs(): array;
}

interface ParagraphRendererInterface
{
    public function render(ParagraphInterface $paragraph): string;
}

abstract class Paragraph implements ParagraphInterface
{
    public $id;
}

class ParagraphText extends Paragraph
{
    public $body;
}

class ParagraphImage extends Paragraph
{
    public $path;
}

class ParagraphGroup implements ParagraphGroupInterface
{
    public function getParagraphs(): array
    {
        return [/* Some paragraphs */]
    }
}

class ParagraphTextRenderer implements ParagraphRendererInterface
{
    public function render(ParagraphInterface $paragraph): string
    {
        // How to have here `ParagraphText` as a type in the argument without downcasting?
    }
}

class ParagraphImageRenderer implements ParagraphRendererInterface
{
    public function render(ParagraphInterface $paragraph): string
    {
        // How to have here `ParagraphImage` as a type in the argument without downcasting?
    }
}

class ParagraphGroupRenderer
{
    /** @var ParagraphRendererInterface[] */
    private $renderers = [/* Array of renderers*/];

    public function render(ParagraphGroupInterface $paragraphGroup): string
    {
        $output = '';
        $paragraphs = $paragraphGroup->getParagraphs();
        foreach ($paragraphs as $paragraph) {
            $output .= $this->renderers[get_class($paragraph)]->render($paragraph);
        }
        return $output;
    }
}

So basically as we can see, we have group of paragraphs that we would like to render (transform into string). Each paragraph has its own renderer, that is responsible for extracting data from the Paragraph object and create a string out of it.

It seems natural for renderers to have injected objects of classes that they are actually rendering, but that's impossible with the above structure without downcasting. At some point, downcasting seems to be unavoidable, at least assuming this structure, and these layers.

Can this structure be reconstructed in such a way that would allow for renderers to have injected specific paragraph classes/interfaces they are suppose to render, and at the same time avoid downcasting by usage of instanceof conditions and such? Are there any patterns that would be better here, and that would avoid downcasting?

Was it helpful?

Solution

I'm not sure you can do this without changing the structure, but as discussed in comments it should be possible with the visitor pattern, which I think would look something like the following. Render is the visitor.

This code has a slightly different intent to the code in the question. The ParagraphGroupRenderer has one renderer of type ParagraphRendererInterface - it doesn't know the concrete type - and uses it to render a collection of ParagraphInterfaces. This is based on the comment from the OP that 'Single type of paragraph can have multiple renderers'. If each paragraph just has its own single renderer then a different solution is likely simpler.

interface ParagraphInterface {
    public function accept(ParagraphRendererInterface $renderer): string;
}

interface ParagraphRendererInterface {
    public function renderParagraphText(ParagraphText $paragraph): string;
    public function renderParagraphImage(ParagraphImage $paragraph): string;
}

class ParagraphText implements ParagraphInterface {

    /** @var string */;
    public $text;
    public function accept(ParagraphRendererInterface $renderer): string
    {
        return $renderer->renderParagraphText($this);
    }
}

class HtmlParagraphRender implements ParagraphRendererInterface{
    public function renderParagraphText(ParagraphText $paragraph): string
    {
       return '<p>' . htmlspecialchars($paragraph->text) . '</p>';
    }

    public function renderParagraphImage(ParagraphImage $paragraph): string
    {
       return '<p> <img src="' . htmlspecialchars($paragraph->source) . '"/></p>';
    }
}

class ParagraphGroup implements ParagraphGroupInterface
{
    /** @return ParagraphInterface[] */
    public function getParagraphs(): array
    {
       return [/* Some paragraphs */]
    }
}

class ParagraphGroupRenderer
{
    /** @var ParagraphRendererInterface */
    private $renderer;

   public function render(ParagraphGroupInterface $paragraphGroup): string
   {
      $output = '';
      $paragraphs = $paragraphGroup->getParagraphs();
      foreach ($paragraphs as $paragraph) {
          $output .= $paragraph->accept($this->renderer);
      }
      return $output;
  }
}
Licensed under: CC-BY-SA with attribution
scroll top