Designing the features and functions of a complex software system like a rich text editor is often a convoluted affair. Here at Ephox, we’ve cut through this murkiness by applying a consistent methodology by which new functionality is evaluated and executed. This methodology touches not only our design process but feeds right into the creation of testable and reusable code.
Great! So how do we go about it?
Well, things tend to kick off with a simple statement like:
“We should have a menu system!”
A worthy goal, but usually we apply a little more pressure and uncover a more nuanced goal, something along the lines of:
“We should have a menu system! […] It should never appear off-screen. The menu system needs to know when it has hit the edge of the container and needs to display the other way when it has no room. It should also handle right-to-left display for languages that require that output, and be reusable, and … and … and …”
Whoa there. When requirements start getting explained, the water seems momentarily clearer, but then things seem to get ever-more complex. That’s where our methodology comes in. When we receive a requirement (well-formed or not), we ask ourselves a few questions:
- What is the core task?
- What related functions will be needed in the near future?
- How can we separate the layers so bugs are easy to track down?
These questions can be used to decompose the problem and ultimately feed into creating modularized code.
In addition, we’ll need to keep in mind that our design should guard against unknowns (there are always things we can’t predict about the browser when embedded in other apps) while making our functions accessible to testing. These issues may be topics for future blog entries.
Working out a Core Task
The first step is to identify the layers of the problem with an eye to reducing them to a core concept. We assume that whatever approach we choose might miss an edge case or two, so it’s well worth it to tease apart the problem into its component parts.
Should this task be its own project (with a witty name)?
With our core concept in hand, we can usually determine whether or not this ‘feels’ like a separate project. Is the concept significant and distinct from others in the editor? If so, it’s a separate project.
This isn’t a hard and fast rule, but in general, we try to bias toward project separation along these lines.
As a result, Textbox.io is highly modularized; we tried to separate things into different projects for both reusability and isolation. The editor is comprised of over 50 individual projects with defined APIs, and this is critical in such a complex environment. A modular architecture lets us check that a piece of functionality is working as expected in the simplest possible environment before adding the complexities of spell-checking, undo management, iframes, UI, etc.
Now comes the best part of making a separate project: choosing a name! For this project, we selected the codename Repartee (repərˈtē): conversation or speech characterized by quick, witty comments or replies. We felt that cartoon speech bubbles best represented the visual concept of menu positioning and, thus, we chose Repartee to describe the concept.
For Repartee, we’ll create a new module project and move our code there. We’ll need to create a demo page as well, which will let us test the functionality in isolation. Finally, we’ll get down to thinking about design: Which API should Repartee have?
Designing a Module API
Designing an API is a tougher question than it might seem. We shouldn’t only consider whether Repartee can do what has currently been asked of it, but we must also think about what will be asked of it in the future. Future drivers for new or customizable functionality need to be considered if the design is going to be reusable and relatively stable. In this case, we might consider the fact that Repartee will need to be tolerant of a future, “docked” toolbar behavior. Certainly, we can’t know much about that future behavior, but we can at least put some thought into keeping Repartee flexible for the future.
To this, we add a grain of salt. As we are often reminded when developing tools are embedded in someone else’s application, there are a large number of things that we just aren’t going to know. As such, we’ll design with an expectation that Repartee will improve iteratively over time. When forming the API, improvement should be able to occur with as few changes as possible to the calling code.
Layer 1: The Algorithm
Now, with the core idea of what Repartee will do understood (or at least imagined), we can now consider Repartee’s core – the data-driven algorithm. Here, at the very lowest layer, we’ll need to consider:
- I have a container box with coordinates and dimensions (x, y, width, and height).
- I have another box that I am trying to fit inside the container box.
- I have a starting point inside the container (x, y).
- I have a list of layout options to try (SE, SW, NE, NW – directions from the starting point such as “SE: down and to the right”).
Given a list of prioritized candidate layouts, we’ll find a suitable layout for the menu that fits within the box, using some heuristic for suitability. The concept can (and should) be unit-tested and even properties-based tested.
Whatever we build at this layer should be deterministic: always returning the same output for a given input. This will go a long way to helping track down bugs (at least letting us rule out the algorithm) further down the line. It is important to note that getting this part right does not ensure that everything else will work. What it does do, however, is allow us to rule out this algorithm as the culprit. The fact that we’ve structured our logic in this way is vital for successful problem decomposition.
Layer 2: Converting DOM Elements to Data
Repartee is responsible for being able to show a DOM element within another DOM element, based on the space available. This means that it will need to be able to convert DOM elements into the raw data that the algorithm requires, as well as interpret the algorithm’s output and apply it back to the elements if necessary.
Once we have this working, it’s time to integrate Repartee into the complete Textbox.io editor.
Layer 3: Integrating with the editor
Once again, we’ll need a part of the editor to interface with Repartee, isolating it from the rest of the editor. Overall we will have three layers:
- Editor Code: Deriving the elements and layouts to send to the project.
- Repartee API: Deriving from the DOM elements the coordinates, dimensions, and layout candidates to send to the algorithm.
- Repartee Algorithm: Choosing the best layout.
- Repartee Algorithm: Choosing the best layout.
- Repartee API: Consolidating the information to send to the editor about displaying the DOM elements.
- Editor Code: Rendering the DOM elements in the right spot.
Although this is a simplification of the actual Textbox.io code, the concepts are the same. We allow each stage of the process to perform only a discrete action, letting us be sure of the result of each step, given an input.
This approach pays huge dividends when considering a bug: By structuring Repartee in this way, we can easily identify which stage of the process is at fault. If we can’t replicate the bug at the algorithm layer or the API layer, then it is easy to rule out Repartee and look elsewhere in the editor code. Similarly, if we can replicate it at the algorithm layer, we only need to fix it there, and everything else should continue working as expected.
Why are there so many layers?
The critical thing when breaking down a feature into different project layers is having very strongly defined inputs and outputs between the layers. Wherever possible, we want all of the non-editor projects to be compatible with anything, not just an editor. We want their required inputs to be as minimal as possible.
The layered approach paid huge dividends when we created Textbox.io’s inline-editing mode. The projects didn’t have any assumptions about the editor; they were only dealing with DOM elements and classes. As such, changing a core concept of how the editor functioned (removing the editing iframe) didn’t matter much to the individual projects; they continued to operate as they had all along, unaware of the big shift in editor behavior.
Similar advantages were seen when Repartee’s requirements changed. When we first developed Repartee, Textbox.io, it did not have strong mobile or RTL-language support. When those requirements came in, Repartee could be substantively changed without having to rewrite large chunks of editor code.
Ultimately, the maintainability and extensibility of Textbox.io’s codebase are our goals. Code should be as conducive as possible to productive refactoring; it is going to happen, so we plan ahead for it. The code we write is almost certainly still not 100% correct, but we structure it in such a way that we can continually address new challenges, as they arise, without having to change code all over the place!
To be fair, the problem-decomposition approach does have drawbacks. Building in this way can appear over-engineered, add to code size, and the levels of indirection and layers can be harder for developers new to the codebase to follow. Though we wrestle with these issues, for us in the Textbox.io project team, modular problem decomposition is worth it.