2
\$\begingroup\$

I had an exercise as below for an interview. I've added my implementation at the end. How could I improve it?

Exercise description:

Develop a simple vector based drawing package.

Your application should support the following 5 drawing primitives (we will call them widget)

1) rectangle 2) square 3) ellipse 4) circle 5) textbox

The application should allow a user to add a new widget to the drawing, stating the location and size / shape of the widget. The location can be a standard x/y coordinate on an imaginary page. The size/shape depends on the widget, as follows:

  • rectangle - width and height
  • square - width
  • ellipse - horizontal and vertical diameter
  • circle - diameter
  • textbox - bounding rectangle (i.e, the rectangle which surronds the textbox; the text will be centred within this rectangle)

Notes:

  • you don't need to actually render the widgets in any manner - we're just simulating a drawing package at this stage
  • your application can add a hard coded set of widgets to the drawing at the bootstrap, no real user input. Please add one of each widget to the drawing.

My implementation:

bootstrap.php

/*
* rectangle x=10, y=10, width=30, height=40
*/
$renderEngine = new CliRenderer();
$renderEngine->render("
-------------------------------\n
Current drawing\n
------------------------------\n
");
$rectangle = new WidgetFactory(new Widget(10,10), new Rectangle(30, 40), $renderEngine);
$rectangle->draw();
/*
* square x=15, y=30, size=35
 */
$square = new WidgetFactory(new Widget(15, 30), new Square(35), $renderEngine);
$square->draw();
/*
* ellipse x=100, y=150, horizontal diameter=300, vertical diameter=200
*/

$ellipse = new WidgetFactory(new Widget(100, 150), new Elipse(300, 200), $renderEngine);
$ellipse->draw();
/**
* circle x=1, y=1, size=300
*/
$circle = new WidgetFactory(new Widget(1,1), new Square(300), $renderEngine);
$circle->draw();
/*
* textbox x=5, y=5, width=200, height=100, text="sample text"
*/
$textBox = new WidgetFactory(new Widget(5,5), new TextBox(200, 100, "sample text"), $renderEngine);
$textBox->draw();
$renderEngine->render("------------------------------");

RenderEngineInterface.php

<?php

interface RenderEngineInterface
{
    public function render($str);
} 

CliRenderer.php

<?php

class CliRenderer implements RenderEngineInterface
{

    public function render($str)
    {
        echo $str."\n";
    }

}

WidgetFactory.php

<?php


class WidgetFactory
{

    private $widget;

    private $shape;

    private $renderEngine;

    function __construct(WidgetInterface $widget,ShapeInterface $shape,RenderEngineInterface $renderEngine)
    {
        $this->widget = $widget;
        $this->shape = $shape;
        $this->renderEngine = $renderEngine;
    }

    public function draw()
    {
        return $this->renderEngine->render($this->shape->output($this->widget));
    }


}

WidgetInterface.php

<?php
interface WidgetInterface
{

    public function getX();
    public function getY();
}

Widget.php

class Widget implements WidgetInterface
{
    private $x;

    private $y;

    function __construct($x, $y)
    {
        $this->x = $x;
        $this->y = $y;
    }

    /**
     * @return mixed
     */
    public function getX()
    {
        return $this->x;
    }

    /**
     * @param mixed $x
     */
    public function setX($x)
    {
        $this->x = $x;
    }

    /**
     * @return mixed
     */
    public function getY()
    {
        return $this->y;
    }

    /**
     * @param mixed $y
     */
    public function setY($y)
    {
        $this->y = $y;
    }
}

ShapeInterface.php

<?php

interface ShapeInterface
{
    public function output(WidgetInterface $widget);
} 

Circle.php

<?php


class Circle implements ShapeInterface
{

    private $size;

    function __construct($size)
    {
        $this->size = $size;
    }

    /**
     * @return mixed
     */
    public function getSize()
    {
        return $this->size;
    }

    /**
     * @param mixed $size
     */
    public function setSize($size)
    {
        $this->size = $size;
    }


    public function output(WidgetInterface $widget)
    {
        return "circle (".$widget->getX().",".$widget->getY().") size=".$this->size;
    }

} 

Elipse.php

<?php


class Elipse implements ShapeInterface
{

    private $diameterH;
    private $diameterV;

    function __construct($diameterH, $diameterV)
    {
        $this->diameterH = $diameterH;
        $this->diameterV = $diameterV;
    }


    public function output(WidgetInterface $widget)
    {
        return "ellipse (".$widget->getX().",".$widget->getY().") horizontal diameter=".$this->diameterH.", vertical diameter=".$this->diameterV;
    }

} 

Rectangle.php

<?php
class Rectangle implements ShapeInterface
{

    private $width;

    private $height;

    function __construct($width,$height)
    {
        $this->height = $height;
        $this->width = $width;
    }

    /**
     * @return mixed
     */
    public function getHeight()
    {
        return $this->height;
    }

    /**
     * @param mixed $height
     */
    public function setHeight($height)
    {
        $this->height = $height;
    }

    /**
     * @return mixed
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * @param mixed $width
     */
    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function output(WidgetInterface $widget)
    {
        return "rectangle (".$widget->getX().",".$widget->getY().") width=".
        $this->width." height=".$this->height;
    }

}

Square.php

<?php

class Square implements ShapeInterface
{

    private $size;

    function __construct($size)
    {
        $this->size = $size;
    }

    /**
     * @return mixed
     */
    public function getSize()
    {
        return $this->size;
    }

    /**
     * @param mixed $size
     */
    public function setSize($size)
    {
        $this->size = $size;
    }


    public function output(WidgetInterface $widget)
    {
        return "square (".$widget->getX().",".$widget->getY().") size=".$this->size;
    }

} 

TextBox.php

<?php

class TextBox implements ShapeInterface
{

    private $width;

    private $height;

    private $text;

    function __construct($width,$height, $text)
    {
        $this->height = $height;
        $this->width = $width;
        $this->text = $text;
    }

    /**
     * @return mixed
     */
    public function getHeight()
    {
        return $this->height;
    }

    /**
     * @param mixed $height
     */
    public function setHeight($height)
    {
        $this->height = $height;
    }

    /**
     * @return mixed
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * @return mixed
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * @param mixed $text
     */
    public function setText($text)
    {
        $this->text = $text;
    }

    /**
     * @param mixed $width
     */
    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function output(WidgetInterface $widget)
    {
        return "textbox (".$widget->getX().",".$widget->getY().") width=".
        $this->width." height=".$this->height;
    }

} 
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

You have written lots of code. I would have preferred a lighter API. Something like this:

<?php

class Canvas {
    private $commands = [];

    public function moveTo($x, $y) {
        return $this->add("M $x $y");
    }

    public function square($width) {
        return $this->rectangle($width, $width);
    }

    public function rectangle($width, $height) {
        return $this->add("h $width v $height h -$width v -$height");
    }

    public function ellipse($hradius, $vradius) {
        return $this->add("TODO: some SVG commands for an ellipse($hradius, $vradius)");
    }

    public function circle($radius) {
        return $this->ellipse($radius, $radius);
    }

    public function textBox($width, $height, $text) {
        $this->rectangle($width, $height);
        return $this->add("\"$text\" tj"); // OK, this is a PDF operator, not SVG
    }

    public function draw() {
        return implode("\n", $this->commands);
    }

    private function add($command) {
        $this->commands[] = $command;
        return $this;
    }
}

The point is that the actual drawing was not part of the exercise. And if you have a little luck you have to draw it to a PDF or PostScript file or an SVG file, in which case you don't need a class per graphic primitive since you can generate the rendering code directly.

Using this canvas class is easy:

print((new Canvas())
    ->moveTo(10, 10)->rectangle(60, 30)
    ->moveTo(10, 50)->square(40)
    ->moveTo(10, 90)->ellipse(10, 30)
    ->moveTo(10, 90)->circle(20)
    ->moveTo(10, 130)->textBox(70, 10, "Hello, world")
    ->draw());

As long as the users of this canvas class don't need to manipulate the graphic primitives, there is no need to make the API more complicated than this.

I have implemented exactly this idea in a practical project, and it works great there.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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