0

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?

  • Why not combine ParagraphInterface and ParagraphRendererInterface. Then render takes no argument (other than the implicit $this argument) and it's standard polymorphisim. – bdsl Jul 19 at 21:17
  • Single responsibility principle. Paragraph is a database entity. Renderer is a way of representing a paragraph. Single type of paragraph can have multiple renderers, one for normal HTML, other for rendering it for AMP or as a JSON for REST API etc. – Luken Jul 19 at 21:51
  • Right, so ideally you'd like double dispatch. I suppose the visitor pattern is an option. – bdsl Jul 19 at 22:08
  • I didn't consider double dispatch but I was reading up on visitor pattern but couldn't figure out how/if can it be applied here usefully, I just couldn't make it work, but maybe someone else have an idea how to use it. I will look into double dispatch, thanks for the suggestion :) . If you have any example in mind, feel free to post an answer. – Luken Jul 19 at 22:19
  • Sorry double dispatch wasn't really a suggestion - that's a language feature that PHP doesn't have. The visitor pattern is designed as a way to work around that limitation. – bdsl Jul 19 at 22:55
0

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;
  }
}
  • This assumes all types of paragraph are renderable by all types of renderer. – bdsl Jul 19 at 23:29
  • The reason why I didn't quite like this solution after I was experimenting with the visitor pattern was that it couples paragraphs with renderers, which is good on one hand, because it guarantees that every paragraph will have a renderer, which is a point of avoiding downcasting, but at same time so tight coupling have serious drawbacks if one would like to create a generic "paragraphs" library. Paragraph class has to reference a specific renderer (in definition of accept), that is completely specific to the application, which makes ParagraphInterface coupled with the logic of specific app. – Luken Jul 23 at 16:50
  • I thought about it and I think I see now why this is the case - it's sort of a trade-off, I feel that thinking about it made me much wiser pattern-wise, thanks. – Luken Jul 23 at 16:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.