Suppose you need to design a drawing program that relies on a geometric kernel library.
Inside the geometric kernel library, you have the definition of Line
, Circle
, Ellipse
classes with all the geometrical data (position, radius) and functions (area, perimeter, tangentAt).
The actual program should extend these geometric kernel classes to add drawing functions but you don’t want to have DrawableLine
or DrawableCircle
classes on the UI side: entities classes must be unique.
The final goal is to have a single collection of entities in the geometric kernel, that can also be used in the drawing application to do the actual drawing.
I am using C# so I cannot use polymorphism.
- The first idea I had is to add a null field to the geometric entity base class (something like
object Renderer
) that would be populated and used in the drawing application to do the actual drawing (at the cost to cast to proper types it all the times).
What other options do I have?
8
Approach #1 (in C# / .Net): Extension Methods
Those are exactly made for this – extending entities of a base library inside a depending library, where the original entities cannot be changed where they “live”.
Of course, this only works when all the drawing operations can be implemented in terms of public attributes and public methods of the geometric classes. But for this kind of use case, this is typically the case or can easily be arranged.
Approach #2: Generic drawing functions inside the geometric entities
Such functions would get some abstract graphics context or canvas object and let the entity draw itself on that canvas. The canvas interface must be part of the kernel, but the concrete canvas implementation would be outside of the kernel, hence the kernel does not become dependend on the specific UI components.
This approach makes most sense when the graphics context interface can provide some basic drawing operations which can be used for all of the geometric objects, maybe a “DrawLineSegment” or “DrawPolylines”, which is universally used for drawing a line, circle or ellipse.
Of course, nothing hinders you to combine both approaches: generic drawing functions working on an abstract canvas can be implemented as extension methods outside the kernel. This keeps the “draw-to-canvas” logic fully away from the kernel, it can be implemented, for example, in some intermediate layer or directly in the application layer.
2
It seems very challenging to design one object to address two different concerns (abstract geometry, and concrete drawing) and at the same time achieve separation of concerns.
One technique sc technique is composition: you may inject some drawing capabilities into a geometric object or some geometric capabilities in a drawable. On both cas you would polute one concern with the other, unless the injected object knows about the shape of the other. Another sc technique is inheritance. But this will inevitably lead to drawableCircle and circle.
Two designs manage well separation of concerns:
- the drawableShape is composed with a Shape. This allows by the way to have several drawableCircle instances for the same geometricCircle instance, and thus works well under MVC and similar architecture.
- using a bridge pattern that lets an abstract shape be composed with a drawable api implementation, leaving both evolve independently. This allows to create shapes that can draw themselves independently of the graphical API that implements the drawing. In your case it seems however to be oberengineering the problem.
At risk of confusing everyone, let’s forget about OOP for a second and talk about the English language. There are two types of objects in English: direct objects and indirect objects. If I give you a book, the book is the direct object and you are the indirect object. If I draw a circle on a wall then the circle is the direct object and the wall is the indirect object.
Seems to me the kernel library is providing the direct objects while the application itself contains the indirect object, which might generically be called a “canvas.” So you can have an interface like this:
class Canvas
{
protected readonly Control _control;
public Canvas(Control controlToReceiveDrawings)
{
_control = controlToReceiveDrawings;
}
public void Draw(Circle circle)
{
using var gr = _control.CreateGraphics();
gr.DrawEllipse
(
pen: new Pen ( circle.Color ),
x: circle.X,
t: circle.Y,
width: circle.Radius * 2,
height: circle.Radius * 2
);
}
public void Draw(Line line)
{
//etc....
}
}
The canvas class is the container for anything that supports rendering (e.g. a reference to a WinForms control, as in this example, although it could also be some other sort of renderer or interface reference) and is its sole responsibility it to act as a bridge between your primitives and the rendering device.
This way you avoid having to extend or contain your primitive classes while still providing a cohesive library.