2

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
  • 1
    Something somewhere has to know how to draw a circle. Where do you want that code to live? It will have to be 1) in the UI layer 2) in the library (probably bad) 3) in some kind of intermediate layer. There are no other options. Oct 20, 2022 at 10:40
  • @PhilipKendall: there is one option you forgot to mention: drawing a circle can be split-up into a coordinate calculation and the actual drawing of some pixels on some UI. Hence the operation as a whole can happen partly in the UI layer, partly in the kernel.
    – Doc Brown
    Oct 20, 2022 at 10:59
  • 1
    "I am using C# so I cannot use class polymorphism." Can you elaborate? The term "class polymorphism" (as opposed to polymorphism) is new to me, is not yielding any search engine results, and unless it's conceptually different from polymorphism, I don't see how your observation applies to C#.
    – Flater
    Oct 20, 2022 at 23:09
  • Do you need to add fields, or just methods?
    – user253751
    Oct 21, 2022 at 16:08
  • 1
    C# supports polymorphism. See learn.microsoft.com/en-us/dotnet/csharp/fundamentals/…
    – bdsl
    Oct 21, 2022 at 19:53

3 Answers 3

1

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
  • 1
    Approach #1 doesn't allow the storage of graphics context data, like an OpenGL display list/vertex buffer for example.
    – abenci
    Oct 20, 2022 at 12:09
  • 2
    @abenci: if you have requirements for storing additional data per entity, introducing some general object ExtData attribute in the kernel objects is surely a viable approach. Another option is to give each entity a unique ID and associate the additional data by using some dictionary (with the IDs as keys). In both cases, however, you need to take care of how and when to dispose the associated resources (but that would be similar true for a solution based on inheritance).
    – Doc Brown
    Oct 20, 2022 at 13:38
3

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.
0

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.

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.