Question

I am trying to figure out how to build most scalable and maintainable architecture for my app.

The app is just about choosing some shape and then editing it.

More precisely, choosing some existing shape from the list(square, rectangle, circle, oval, pentagon, N-gon, "house", some custom shape) and then after choosing it, another page appears(slides up for example), where you should be able to edit it.

Would be rather simple if it was just about editing vertices and moving them around, but that would be useless for this exact app, Because in my case each shape has its own editing possibility.

For example: With a square or some regular N-Gon chosen, you can only change its side length(there will be one slider, or textbox somewhere and all sides will scale accordingly). That's easy.

But it gets harder pretty fast:

  • With a rectangle, you can change its width and length. (2 sliders)

  • With trapezium, you can change length of each of its sides but the height should stay same(cause 2 paralell lines), or you could change its angles instead. (4 sliders for lengths and something else for 4 angles)

  • With circle its also easy, just edit the diameter. (1 slider)

  • With oval? 2 diamaters. (2 sliders)

  • With shapes like house(2d house shape): You can change its height, width and roof height. (5 sliders for each side and something for 5 angles)

  • Custom shape, edit length & angles for each side.

I hope these examples are enough to demonstrate why I am stuck so much. I can't find common multiple of all this stuff that I could abstract out(Well except that all these shapes are made of vertices, oh wait, Circle & Oval don't have any vertices. DAMN!).

I have one straightforward idea of doing this, by putting tons of "dumb" work, I will be able to finish this app in a month, but It won't be scalable or maintainable in any way. Its a dumb way of doing it (Basically will be equivalent of writing separate application for each shape).

The app is for Android written on React Native, so I am using SVGs(for performance, obviously to make this even harder).

I already have "Engine" that is able to draw & animate SVGs from supplied vertices.

Currently just parsing vertices anti-clockwise and building a shape.

enter image description here

enter image description here

Thanks.

Was it helpful?

Solution

Not sure if you'll find this answer useful because it's going to be fairly general, and you may have to figure out how to adapt these ideas to React Native, but here are a few thoughts.

Don't try to find some universal commonality in the details of how shapes are manipulated, because these are all very different. You can't base your design on that.

I presume what you want, architecturally, is the ability to relatively easily extend the application with new shapes - without having to change things all over the codebase.

So, on a high level, think of it like this - for each shape, you need (1) a representation of that shape + (2) a UI for manipulating that shape. So what you want when adding a new shape is, ideally, to just define (1) & (2) - perhaps as a couple of new classes - and plug them into the existing application, without doing much else.

Another aspect that's helpful to consider, design-wise, is that your shapes play two distinct roles in the application.

One role is that of a drawable (or renderable). I.e., something that can be drawn (or, rather, knows how to draw itself). When the application needs to draw something, it doesn't need to know what it is, what shape it has, what are its logical properties. In the simplest case, it just needs to know that it has a draw() method (it can be asked to draw itself). Or in a more flexible alternative, a draw(graphicsContext) method - so that it can be told onto which surface it should draw itself (e.g., if you have several drawing surfaces, like the main canvas, and a preview of the shape in a shape picker). The graphicsContext parameter here is the object on which you can call primitive draw methods (I'm thinking of something like the HTML canvas object here). Or, if you are using SVG, draw() (or render()) could produce the appropriate SVG markup. In any case, it would be some variation of that idea.
This means that, to the component that governs the drawing, all these objects can look the same. It just needs a list of IDrawable-s.

The other role is that of an object being manipulated by a particular shape manipulation UI. In this case, each shape looks distinct, and supports a distinct set of methods. The question here is, when you detect a click on a particular shape, how do you determine the type of the shape, in order to instantiate the correct user interface.

One way to do it is to somehow query for the shape type (you could use some inbuilt type querying mechanism, or simply have a type-discriminating property on the shape), and then type cast as necessary.

But, you could do it in a different way. Let's think this through. You have the "canvas" - the workspace where the shapes are shown. The shapes need to be drawable and clickable/dragable. You have the UI area (say, a separate <div>) where you can inject the UI for manipulating the shape. The application doesn't need to know what the UI is, it just needs to be able to get it from somewhare, so that it can inject it and show it. What if the shape, when clicked, was able to provide this UI object?

So now you have something like this. The "shapes" that the application works with at a higher level are not necessarily your "raw" shape classes. Instead, it's a small component - an object that contains an underlying reference to the actual shape, can draw itself by drawing the shape, and can provide on-demand UI for manipulating the shape, since it knows what the type of the shape is. This kind of setup also enables you to "embellish" the shape when you draw it - e.g., you may want to indicate that the shape is active/selected, or put manipulation handles around it, etc.

The provided shape manipulation UI object is also self contained - a small smart control. It has the reference to the underlying concrete shape (basically, the shape is its state) - typed appropriately. The provider could inject the shape instance.

This could be summarized like this - it's by no means "the one true solution", but it's something to get you started thinking in a different direction:

Shape classes / data structures:

  • each represents a certain shape
  • aren't part of a hierachy

For each shape:

  • Shape Drawable component:

    • contains a specific shape
    • can draw itself - IDrawable
    • can provide shape-specific manipulation UI - IShapeManipulatorProvider
    • can handle clicks, certain user actions, etc.
  • Shape Manipulation UI:

    • has a reference to a specific shape instance
    • provides controls for manipulating that instance
    • since it changes the same underlying shape that's referenced by the Shape Drawable, the shape will be redrawn on the next update cycle

High level application:

  • has a list (or maybe a tree) of Shape Drawable components representing the state of the canvas
  • delegates drawing to them
  • when one of them is clicked, asks it to provide the manipulation UI for it; injects that UI at the appropriate place

Adding a new kind of shape to the application would consist of defining the three components (shape, shape drawable, shape manipulation UI), wiring them up, and plugging them in.

Your shape drawables could also act as prototype instances for the shapes. E.g., if you need to show a menu/toolbar letting the user know what shapes are possible to choose from, you can populate the menu with different shape drawables. The menu/toolbar doesn't need to know what shapes they are, again, it just need to know how to draw them and to detect clicks. When the user selects one, you can clone it and place it on the canvas (cloning from a prototypical instance like this is the idea behind the prototype pattern). Basically, in this scenario, when adding support for a new kind of shape, after wiring up together, you'd only need to plug in the shape drawable as an extra menu/toolbar item.

Finally, you can create composite shapes by composing shape drawables - see the composite pattern. At the basic level, you can provide some simple controls to manipulate the composite as a whole (e.g., resizing). For things that are more involved, you'll have to figure out how to do things like grouping/ungrouping, or isolating the composite shape to manipulate its internals before exiting the isolation mode and placing it back on the main canvas, etc.


P.S. Don't get too attached to the names I used ("Shape Drawable", "IShapeManipulatorProvider", ...), it's just something that I came up with when writing this answer - if you come up with nicer names that are better suited to your application, by all means, use those.

Licensed under: CC-BY-SA with attribution
scroll top