A colleague of mine showed me the following result.
----------------------------------------------------------------
Current Drawing
----------------------------------------------------------------
Rectangle (10,10) width=30 height=40
Square (15,30) size=35
Ellipse (100,150) diameterH = 300 diameterV = 200
Circle (1,1) size=300
Textbox (5,5) width=200 height=100 Text="sample text"
He asked me whether I can write a simple console application to achieve the above result. Simple task you might say.
Well.. it is pretty obvious that we need to start off with an abstract "Shape" class. But what about the logic to "draw" the shape on the canvas. What we need is an interface like "ICanDraw", which contains the "draw" method. Now, the Shapes can be drawn, therefore the Shape should implement the "ICanDraw" interface. But hang on, each Shape should present itself to the canvas. The canvas should then decide how to "draw" the shape on the canvas.
The most interesting piece here is the text box. A text box is not strictly a shape, but a "Rectangle" with a additional text property. We can say that a text box is composed of a Rectangle. Internally the text box will use the Rectangle to represent itself.
See the following UML.
My "ICanDraw" interface looks like below.
/// <summary>
/// This interface indicates that the shape can be drawn.
/// </summary>
public interface ICanDraw
{
void Draw(ICanvas canvas);
}
The abstract "Shape" class looks like below.
/// <summary>
/// This is the base class for a shape.
/// </summary>
public abstract class Shape : ICanDraw
{
public int X { get; private set; }
public int Y { get; private set; }
protected Shape(int x, int y)
{
X = x;
Y = y;
}
public abstract void Draw(ICanvas canvas);
}
Keep a close eye on the abstract "Draw" method. Also notice that the "X" and "Y" are "readonly" properties. This plays nicely to the immutability of objects
The "Circle" class looks like below:
/// <summary>
/// This is the concrete implementation of a circle.
/// </summary>
public class Circle : Shape
{
public int Size { get; private set; }
public Circle(int x, int y, int size)
: base(x, y)
{
Size = size;
}
public override void Draw(ICanvas canvas)
{
canvas.Draw(this);
}
}
The code for the text box looks like below.
/// <summary>
/// This is the concrete implementation of a text box.
/// </summary>
public class Textbox : ICanDraw
{
public Rectangle Rectangle { get; private set; }
public string Content { get; private set; }
public Textbox(int x, int y, int width, int height, string content)
{
Rectangle = new Rectangle(x, y, width, height);
Content = content;
}
public void Draw(ICanvas canvas)
{
canvas.Draw(this);
}
}
Ok, we have all the concrete classes and a special text box.
Next piece of the puzzle is the "ICanvas". Since we are just writing the content to the console, this will be a special console canvas. The canvas is aware of the shapes and will decide how to draw them. The responsibility to draw a shape is now with the canvas.
The following is the special implementation of "ICanvas" for the console.
/// <summary>
/// This is the "canvas" representing the console.
/// </summary>
public class ConsoleCanvas : ICanvas
{
public void Draw(Circle item)
{
Console.WriteLine("Circle({0},{1}) Size={2}", item.X, item.Y, item.Size);
}
public void Draw(Rectangle item)
{
Console.WriteLine("Rectangle({0},{1}) Width={2}, Height={3}", item.X, item.Y, item.Width, item.Height);
}
public void Draw(Square item)
{
Console.WriteLine("Square({0},{1}) Size={2}", item.X, item.Y, item.Size);
}
public void Draw(Ellipse item)
{
Console.WriteLine("Ellipse({0},{1}) DiameterH={2}, Diameter={3}", item.X, item.Y, item.DiameterH, item.DiameterV);
}
public void Draw(Textbox item)
{
Console.WriteLine("TextBox({0},{1}) Width={2}, Height={3}, Text={4}", item.Rectangle.X, item.Rectangle.Y, item.Rectangle.Width, item.Rectangle.Height, item.Content);
}
}
As you can see the canvas is aware of the shapes and it decides how to draw them. The canvas will determine this by the runtime type of the object.
Can you see a pattern here? This is in fact the "Visitor" pattern. The purpose of the visitor pattern is to choose a functionality depending on the run-time type of an object. The pattern is also used to introduce additional functionality to an object.
So how do we "draw" these shapes. See below.
private static void Load()
{
var items = new List<ICanDraw>()
{
new Rectangle(10, 10, 30, 40),
new Square(15, 30, 35),
new Ellipse(100, 150, 300, 200),
new Circle(1, 1, 300),
new Textbox(5, 5, 200, 100, "Sample Text")
};
items.ForEach(item => item.Draw(new ConsoleCanvas()));
}
The special "ConsoleCanvas" visits each shape and "draws" it on the console.
In terms of SOLID principals, the "Shape" class is
open for extensions and
closed for modifications . Yes, the Open/Close principal.
But can the same functionality be achieved through another pattern?